Skip to content

Commit 3b4e327

Browse files
committed
Squashed commit of the following:
commit aa0a21d Author: Jacob Gelman <[email protected]> Date: Thu Feb 13 06:28:21 2025 -0800 Simplify broadcast extension setup with standard format for identifiers (livekit#573) When configuring a broadcast extension, manually setting the info keys `RTCAppGroupIdentifier` and `RTCScreenSharingExtension` is no longer required when using the standard format. The standard format is as follows: - App group: `group.<main-app-bundle-id>` - Broadcast extension: `<main-app-bundle-id>.broadcast` --------- Co-authored-by: Hiroshi Horie <[email protected]> commit c3ee701 Author: Jacob Gelman <[email protected]> Date: Thu Feb 13 06:05:00 2025 -0800 Deprecate public broadcast picker extension (livekit#586) Public show method defined as an extension to `RPSystemBroadcastPickerView` has been deprecated in favor of `BroadcastManager.shared.requestActivation()`. --------- Co-authored-by: Hiroshi Horie <[email protected]> commit 0da6660 Author: Jacob Gelman <[email protected]> Date: Thu Feb 13 05:55:25 2025 -0800 Release automation (livekit#579) - Add version and platform compatibility badges from [Swift Package Index](https://swiftpackageindex.com/) to README - Automatically updated on each release - Add [nanpa](https://github.com/nbsp/nanpa) configuration - Custom script bumps version across repo (currently Podspec, README, and LiveKitSDK class) - GitHub publish workflow (based on workflow from livekit/rust-sdks) - Create workflow to push new releases to Cocopods when a release is published on GitHub I have also added changeset files to my currently open PRs (livekit#565, livekit#576, and livekit#573) that can be used to test this configuration. --------- Co-authored-by: Hiroshi Horie <[email protected]> commit 5b031c8 Author: Hiroshi Horie <[email protected]> Date: Thu Feb 13 22:36:35 2025 +0900 Update Podspec (livekit#587) Fixes: livekit#566 commit 485e76d Author: Hiroshi Horie <[email protected]> Date: Wed Feb 12 14:36:48 2025 +0900 macOS screen share audio (livekit#561)
1 parent 4a44ee9 commit 3b4e327

20 files changed

+380
-59
lines changed

.github/workflows/bump.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Bump version
2+
on:
3+
workflow_dispatch:
4+
env:
5+
PACKAGE_NAME: client-sdk-swift
6+
jobs:
7+
bump:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: write
11+
steps:
12+
- uses: actions/checkout@v3
13+
with:
14+
ssh-key: ${{ secrets.NANPA_KEY }}
15+
- uses: nbsp/ilo@v1
16+
with:
17+
packages: ${{ env.PACKAGE_NAME }}

.github/workflows/cocoapods.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Push to Cocoapods
2+
on:
3+
workflow_dispatch:
4+
release:
5+
types: [published]
6+
env:
7+
PODSPEC_FILE: LiveKitClient.podspec
8+
jobs:
9+
build:
10+
runs-on: macos-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
- name: Install Cocoapods
14+
run: gem install cocoapods
15+
- name: Validate Podspec
16+
run: pod lib lint --allow-warnings
17+
- name: Publish to CocoaPods
18+
run: pod trunk push ${{ env.PODSPEC_FILE }} --allow-warnings --verbose
19+
env:
20+
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}

.nanpa/.keep

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add changeset files in this directory.
2+
3+
See nanpa documentation for more info:
4+
https://github.com/nbsp/nanpa/blob/trunk/doc/nanpa-changeset.5.scd
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="change" "Simplify broadcast extension setup with standard format for identifiers"

.nanpa/make-extension-private.kdl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="deprecated" "Deprecated public method to show broadcast picker"

.nanparc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version 2.1.1
2+
name client-sdk-swift
3+
custom ./scripts/replace_version.sh

Docs/ios-screen-sharing.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,17 @@ flowchart LR
4444

4545
To use the Broadcast Capture mode, follow these steps to add a Broadcast Upload Extension target and associated configuration to your project. You can also refer to the [example app](https://github.com/livekit-examples/swift-example), which demonstrates this configuration.
4646

47-
#### 1. Add Broadcast Upload Extension Target
4847

48+
#### 1. Add Broadcast Upload Extension Target
4949

5050
<img src="Resources/new-target-options.png" width="500" />
5151

5252
1. In Xcode, Choose "File" > "New > "Target"
5353
2. From the template chooser, select "Broadcast Upload Extension"
54-
3. Name the extension (e.g. "BroadcastExtension"). Take note of the extension's bundle identifier, as it will be needed later.
55-
4. Replace the default content of `SampleHandler.swift` in the new target with the following:
54+
3. Name the extension (e.g. "BroadcastExtension").
55+
4. Press "Finish"
56+
5. From the "Signing & Capabilities" tab of the new target, change the bundle identifier to be the same as your main app with `.broadcast` added to the end. To use a custom identifier, see *[Custom Identifiers](#custom-identifiers)* below.
57+
6. Replace the default content of `SampleHandler.swift` in the new target with the following:
5658

5759
```swift
5860
import LiveKit
@@ -75,14 +77,9 @@ In order for the broadcast extension to communicate with your app, they must be
7577
2. Select the "Signing & Capabilities" tab and press the "+ Capability" button.
7678
3. Add the "App Groups" capability.
7779
4. Press "+" to add a new app group.
78-
5. Enter an app group identifier in the format `group.<domain>.<group_name>`. Be sure to use the same identifier for both targets.
79-
80-
#### 3. Add Properties to Info.plist
81-
82-
1. Set `RTCAppGroupIdentifier` in the Info.plist of **both targets** to the group identifier from the previous step.
83-
2. Set `RTCScreenSharingExtension` in the Info.plist of your **primary app target** to the broadcast extension's bundle identifier.
80+
5. Add the target to the group `group.<main-app-bundle-id>`. To use a custom identifier, see *[Custom Identifiers](#custom-identifiers)* below.
8481

85-
#### 4. Begin Screen Share
82+
#### 3. Begin Screen Share
8683

8784
With setup of the broadcast extension complete, broadcast capture will be used by default when enabling screen share:
8885
```swift
@@ -102,6 +99,8 @@ While running your app in a debug session in Xcode, check the debug console for
10299

103100
### Advanced Usage
104101

102+
#### Manual Track Publication
103+
105104
When using broadcast capture, a broadcast can be initiated externally (for example, via control center). By default, when a broadcast begins, the local participant automatically publishes a screen share track. In some cases, however, you may want to handle track publication manually. You can achieve this by using `BroadcastManager`:
106105

107106
First, disable automatic track publication:
@@ -111,7 +110,7 @@ BroadcastManager.shared.shouldPublishTrack = false
111110

112111
Then, use one of the two methods for detecting changes in the broadcast state:
113112

114-
#### Combine Publisher
113+
##### Combine Publisher
115114
```swift
116115
let subscription = BroadcastManager.shared
117116
.isBroadcastingPublisher
@@ -120,7 +119,7 @@ let subscription = BroadcastManager.shared
120119
}
121120
```
122121

123-
#### Delegate
122+
##### Delegate
124123
```swift
125124
class MyDelegate: BroadcastManagerDelegate {
126125
func broadcastManager(didChangeState isBroadcasting: Bool) {
@@ -129,3 +128,8 @@ class MyDelegate: BroadcastManagerDelegate {
129128
}
130129
BroadcastManager.shared.delegate = MyDelegate()
131130
```
131+
132+
#### Custom Identifiers
133+
134+
By default, the app group identifier is expected to be `group.<main-app-bundle-id>`, and the broadcast extension's bundle identifier is expected to be `<main-app-bundle-id>.broadcast`.
135+
To override these values, set `RTCAppGroupIdentifier` in Info.plist for both targets (both broadcast extension and main app), and set `RTCScreenSharingExtension` in Info.plist for your main app.

LiveKitClient.podspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ Pod::Spec.new do |spec|
88

99
spec.ios.deployment_target = "13.0"
1010
spec.osx.deployment_target = "10.15"
11+
spec.tvos.deployment_target = "17.0"
12+
spec.visionos.deployment_target = "1.0"
1113

1214
spec.swift_versions = ["5.7"]
1315
spec.source = {:git => "https://github.com/livekit/client-sdk-swift.git", :tag => "2.1.1"}
1416

1517
spec.source_files = "Sources/**/*"
1618

17-
spec.dependency("LiveKitWebRTC", "= 125.6422.11")
19+
spec.dependency("LiveKitWebRTC", "= 125.6422.18")
1820
spec.dependency("SwiftProtobuf")
1921
spec.dependency("Logging")
2022

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
Use this SDK to add realtime video, audio and data features to your Swift app. By connecting to <a href="https://livekit.io/">LiveKit</a> Cloud or a self-hosted server, you can quickly build applications such as multi-modal AI, live streaming, or video calls with just a few lines of code.
1515
<!--END_DESCRIPTION-->
1616

17+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Flivekit%2Fclient-sdk-swift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/livekit/client-sdk-swift)
18+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Flivekit%2Fclient-sdk-swift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/livekit/client-sdk-swift)
19+
1720
## Docs & Example app
1821

1922
> [!NOTE]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
19+
#if swift(>=5.9)
20+
internal import LiveKitWebRTC
21+
#else
22+
@_implementationOnly import LiveKitWebRTC
23+
#endif
24+
25+
public final class DefaultMixerAudioObserver: AudioEngineObserver, Loggable {
26+
public var next: (any AudioEngineObserver)? {
27+
get { _state.next }
28+
set { _state.mutate { $0.next = newValue } }
29+
}
30+
31+
/// Adjust the volume of captured app audio. Range is 0.0 ~ 1.0.
32+
public var appVolume: Float {
33+
get { _state.read { $0.appMixerNode.outputVolume } }
34+
set { _state.mutate { $0.appMixerNode.outputVolume = newValue } }
35+
}
36+
37+
/// Adjust the volume of microphone audio. Range is 0.0 ~ 1.0.
38+
public var micVolume: Float {
39+
get { _state.read { $0.micMixerNode.outputVolume } }
40+
set { _state.mutate { $0.micMixerNode.outputVolume = newValue } }
41+
}
42+
43+
// MARK: - Internal
44+
45+
var appAudioNode: AVAudioPlayerNode {
46+
_state.read { $0.appNode }
47+
}
48+
49+
var micAudioNode: AVAudioPlayerNode {
50+
_state.read { $0.micNode }
51+
}
52+
53+
var isConnected: Bool {
54+
_state.read { $0.isConnected }
55+
}
56+
57+
struct State {
58+
var next: (any AudioEngineObserver)?
59+
60+
// AppAudio
61+
public let appNode = AVAudioPlayerNode()
62+
public let appMixerNode = AVAudioMixerNode()
63+
64+
// Not connected for device rendering mode.
65+
public let micNode = AVAudioPlayerNode()
66+
public let micMixerNode = AVAudioMixerNode()
67+
68+
public var isConnected: Bool = false
69+
}
70+
71+
let _state = StateSync(State())
72+
73+
public init() {}
74+
75+
public func setNext(_ handler: any AudioEngineObserver) {
76+
next = handler
77+
}
78+
79+
public func engineDidCreate(_ engine: AVAudioEngine) {
80+
let (appNode, appMixerNode, micNode, micMixerNode) = _state.read {
81+
($0.appNode, $0.appMixerNode, $0.micNode, $0.micMixerNode)
82+
}
83+
84+
engine.attach(appNode)
85+
engine.attach(appMixerNode)
86+
engine.attach(micNode)
87+
engine.attach(micMixerNode)
88+
89+
// Invoke next
90+
next?.engineDidCreate(engine)
91+
}
92+
93+
public func engineWillRelease(_ engine: AVAudioEngine) {
94+
// Invoke next
95+
next?.engineWillRelease(engine)
96+
97+
let (appNode, appMixerNode, micNode, micMixerNode) = _state.read {
98+
($0.appNode, $0.appMixerNode, $0.micNode, $0.micMixerNode)
99+
}
100+
101+
engine.detach(appNode)
102+
engine.detach(appMixerNode)
103+
engine.detach(micNode)
104+
engine.detach(micMixerNode)
105+
}
106+
107+
public func engineWillConnectInput(_ engine: AVAudioEngine, src: AVAudioNode?, dst: AVAudioNode, format: AVAudioFormat, context: [AnyHashable: Any]) {
108+
// Get the main mixer
109+
guard let mainMixerNode = context[kRTCAudioEngineInputMixerNodeKey] as? AVAudioMixerNode else {
110+
// If failed to get main mixer, call next and return.
111+
next?.engineWillConnectInput(engine, src: src, dst: dst, format: format, context: context)
112+
return
113+
}
114+
115+
// Read nodes from state lock.
116+
let (appNode, appMixerNode, micNode, micMixerNode) = _state.read {
117+
($0.appNode, $0.appMixerNode, $0.micNode, $0.micMixerNode)
118+
}
119+
120+
// TODO: Investigate if possible to get this format prior to starting screen capture.
121+
// <AVAudioFormat 0x600003055180: 2 ch, 48000 Hz, Float32, deinterleaved>
122+
let appAudioNodeFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32,
123+
sampleRate: format.sampleRate, // Assume same sample rate
124+
channels: 2,
125+
interleaved: false)
126+
127+
log("Connecting app -> appMixer -> mainMixer")
128+
// appAudio -> appAudioMixer -> mainMixer
129+
engine.connect(appNode, to: appMixerNode, format: appAudioNodeFormat)
130+
engine.connect(appMixerNode, to: mainMixerNode, format: format)
131+
132+
// src is not null if device rendering mode.
133+
if let src {
134+
log("Connecting src (device) to micMixer -> mainMixer")
135+
// mic (device) -> micMixer -> mainMixer
136+
engine.connect(src, to: micMixerNode, format: format)
137+
}
138+
139+
// TODO: Investigate if possible to get this format prior to starting screen capture.
140+
let micNodeFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32,
141+
sampleRate: format.sampleRate, // Assume same sample rate
142+
channels: 1, // Mono
143+
interleaved: false)
144+
145+
log("Connecting micAudio (player) to micMixer -> mainMixer")
146+
// mic (player) -> micMixer -> mainMixer
147+
engine.connect(micNode, to: micMixerNode, format: micNodeFormat)
148+
// Always connect micMixer to mainMixer
149+
engine.connect(micMixerNode, to: mainMixerNode, format: format)
150+
151+
_state.mutate { $0.isConnected = true }
152+
153+
// Invoke next
154+
next?.engineWillConnectInput(engine, src: src, dst: dst, format: format, context: context)
155+
}
156+
}

Sources/LiveKit/Broadcast/BroadcastBundleInfo.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ import Foundation
2020

2121
enum BroadcastBundleInfo {
2222
/// Identifier of the app group shared by the primary app and broadcast extension.
23-
@BundleInfo("RTCAppGroupIdentifier")
24-
static var groupIdentifier: String?
25-
23+
static var groupIdentifier: String? {
24+
if let override = groupIdentifierOverride { return override }
25+
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil }
26+
let appBundleIdentifier = bundleIdentifier.dropSuffix(".\(extensionSuffix)") ?? bundleIdentifier
27+
return "group.\(appBundleIdentifier)"
28+
}
29+
2630
/// Bundle identifier of the broadcast extension.
27-
@BundleInfo("RTCScreenSharingExtension")
28-
static var screenSharingExtension: String?
31+
static var screenSharingExtension: String? {
32+
if let override = screenSharingExtensionOverride { return override }
33+
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil }
34+
return "\(bundleIdentifier).\(extensionSuffix)"
35+
}
2936

3037
/// Path to the socket file used for interprocess communication.
3138
static var socketPath: SocketPath? {
@@ -37,7 +44,14 @@ enum BroadcastBundleInfo {
3744
static var hasExtension: Bool {
3845
socketPath != nil && screenSharingExtension != nil
3946
}
47+
48+
@BundleInfo("RTCAppGroupIdentifier")
49+
private static var groupIdentifierOverride: String?
4050

51+
@BundleInfo("RTCScreenSharingExtension")
52+
private static var screenSharingExtensionOverride: String?
53+
54+
private static let extensionSuffix = "broadcast"
4155
private static let socketFileDescriptor = "rtc_SSFD"
4256

4357
private static func socketPath(for groupIdentifier: String) -> SocketPath? {
@@ -49,4 +63,12 @@ enum BroadcastBundleInfo {
4963
}
5064
}
5165

66+
private extension String {
67+
func dropSuffix(_ suffix: String) -> Self? {
68+
guard hasSuffix(suffix) else { return nil }
69+
let trailingIndex = index(endIndex, offsetBy: -suffix.count)
70+
return String(self[..<trailingIndex])
71+
}
72+
}
73+
5274
#endif

Sources/LiveKit/Broadcast/BroadcastManager.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ public final class BroadcastManager: Sendable {
7070
///
7171
public func requestActivation() {
7272
Task {
73-
await RPSystemBroadcastPickerView.show(
74-
for: BroadcastBundleInfo.screenSharingExtension,
75-
showsMicrophoneButton: false
73+
await RPSystemBroadcastPickerView.showPicker(
74+
for: BroadcastBundleInfo.screenSharingExtension
7675
)
7776
}
7877
}
@@ -116,4 +115,17 @@ public protocol BroadcastManagerDelegate {
116115
func broadcastManager(didChangeState isBroadcasting: Bool)
117116
}
118117

118+
private extension RPSystemBroadcastPickerView {
119+
/// Convenience function to show broadcast picker.
120+
static func showPicker(for preferredExtension: String?) {
121+
let view = RPSystemBroadcastPickerView()
122+
view.preferredExtension = preferredExtension
123+
view.showsMicrophoneButton = false
124+
125+
let selector = NSSelectorFromString("buttonPressed:")
126+
guard view.responds(to: selector) else { return }
127+
view.perform(selector, with: nil)
128+
}
129+
}
130+
119131
#endif

0 commit comments

Comments
 (0)