Skip to content

Commit 15a8022

Browse files
authored
Extract agent components (#21)
1 parent af58685 commit 15a8022

File tree

11 files changed

+203
-247
lines changed

11 files changed

+203
-247
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
),
1818
],
1919
dependencies: [
20-
.package(url: "https://github.com/livekit/client-sdk-swift.git", from: "2.0.17"),
20+
.package(url: "https://github.com/livekit/client-sdk-swift.git", from: "2.6.0"),
2121
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"),
2222
],
2323
targets: [

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ let package = Package(
1919
),
2020
],
2121
dependencies: [
22-
.package(url: "https://github.com/livekit/client-sdk-swift.git", from: "2.0.17"),
22+
.package(url: "https://github.com/livekit/client-sdk-swift.git", from: "2.6.0"),
2323
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"),
2424
],
2525
targets: [

[email protected]

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// swift-tools-version:6.0
2+
// (Xcode16.0+)
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "LiveKitComponents",
8+
platforms: [
9+
.iOS(.v14),
10+
.macOS(.v11),
11+
.macCatalyst(.v14),
12+
.tvOS(.v17),
13+
.visionOS(.v1),
14+
],
15+
products: [
16+
.library(
17+
name: "LiveKitComponents",
18+
targets: ["LiveKitComponents"]
19+
),
20+
],
21+
dependencies: [
22+
.package(url: "https://github.com/livekit/client-sdk-swift.git", from: "2.6.0"),
23+
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"),
24+
],
25+
targets: [
26+
.target(
27+
name: "LiveKitComponents",
28+
dependencies: [
29+
.product(name: "LiveKit", package: "client-sdk-swift"),
30+
],
31+
path: "Sources"
32+
),
33+
.testTarget(
34+
name: "LiveKitComponentsTests",
35+
dependencies: ["LiveKitComponents"]
36+
),
37+
],
38+
swiftLanguageModes: [
39+
.v5, // opt-out from dynamic actor isolation checks
40+
.v6,
41+
]
42+
)

Sources/LiveKitComponents/Audio/AudioProcessor.swift

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,60 +14,62 @@
1414
* limitations under the License.
1515
*/
1616

17-
import AVFoundation
17+
@preconcurrency import AVFoundation
1818
import LiveKit
1919

20-
public class AudioProcessor: ObservableObject, AudioRenderer {
20+
@MainActor
21+
public final class AudioProcessor: ObservableObject, AudioRenderer {
2122
public let isCentered: Bool
2223
public let smoothingFactor: Float
2324

24-
// Normalized to 0.0-1.0 range.
25-
@Published public var bands: [Float]
25+
/// Normalized to 0.0-1.0 range.
26+
@Published public private(set) var bands: [Float]
2627

27-
private let _processor: AudioVisualizeProcessor
28-
private weak var _track: AudioTrack?
28+
private let processor: AudioVisualizeProcessor
29+
private weak var track: AudioTrack?
2930

3031
public init(track: AudioTrack?,
3132
bandCount: Int,
3233
isCentered: Bool = true,
33-
smoothingFactor: Float = 0.3)
34+
smoothingFactor: Float = 0.25)
3435
{
3536
self.isCentered = isCentered
3637
self.smoothingFactor = smoothingFactor
3738
bands = Array(repeating: 0.0, count: bandCount)
3839

39-
_processor = AudioVisualizeProcessor(bandsCount: bandCount)
40-
_track = track
41-
_track?.add(audioRenderer: self)
40+
processor = AudioVisualizeProcessor(bandsCount: bandCount)
41+
42+
self.track = track
43+
track?.add(audioRenderer: self)
4244
}
4345

4446
deinit {
45-
_track?.remove(audioRenderer: self)
47+
track?.remove(audioRenderer: self)
4648
}
4749

48-
public func render(pcmBuffer: AVAudioPCMBuffer) {
49-
let newBands = _processor.process(pcmBuffer: pcmBuffer)
50-
guard var newBands else { return }
51-
52-
// If centering is enabled, rearrange the normalized bands
53-
if isCentered {
54-
newBands.sort(by: >)
55-
newBands = centerBands(newBands)
56-
}
50+
public nonisolated func render(pcmBuffer: AVAudioPCMBuffer) {
51+
Task {
52+
let newBands = await processor.process(pcmBuffer: pcmBuffer)
53+
guard var newBands else { return }
5754

58-
DispatchQueue.main.async { [weak self] in
59-
guard let self else { return }
55+
// If centering is enabled, rearrange the normalized bands
56+
if isCentered {
57+
newBands.sort(by: >)
58+
newBands = Self.centerBands(newBands)
59+
}
6060

61-
self.bands = zip(self.bands, newBands).map { old, new in
62-
self._smoothTransition(from: old, to: new, factor: self.smoothingFactor)
61+
await MainActor.run { [newBands] in
62+
bands = zip(bands, newBands).map { old, new in
63+
Self.smoothTransition(from: old, to: new, factor: smoothingFactor)
64+
}
6365
}
6466
}
6567
}
6668

6769
// MARK: - Private
6870

6971
/// Centers the sorted bands by placing higher values in the middle.
70-
@inline(__always) private func centerBands(_ sortedBands: [Float]) -> [Float] {
72+
@inline(__always) private nonisolated static func centerBands(_ sortedBands: [Float]) -> [Float] {
7173
var centeredBands = [Float](repeating: 0, count: sortedBands.count)
7274
var leftIndex = sortedBands.count / 2
7375
var rightIndex = leftIndex
@@ -88,17 +90,17 @@ public class AudioProcessor: ObservableObject, AudioRenderer {
8890
}
8991

9092
/// Applies an easing function to smooth the transition.
91-
@inline(__always) private func _smoothTransition(from oldValue: Float, to newValue: Float, factor: Float) -> Float {
93+
@inline(__always) private nonisolated static func smoothTransition(from oldValue: Float, to newValue: Float, factor: Float) -> Float {
9294
// Calculate the delta change between the old and new value
9395
let delta = newValue - oldValue
9496
// Apply an ease-in-out cubic easing curve
95-
let easedFactor = _easeInOutCubic(t: factor)
97+
let easedFactor = easeInOutCubic(t: factor)
9698
// Calculate and return the smoothed value
9799
return oldValue + delta * easedFactor
98100
}
99101

100102
/// Easing function: ease-in-out cubic
101-
@inline(__always) private func _easeInOutCubic(t: Float) -> Float {
103+
@inline(__always) private nonisolated static func easeInOutCubic(t: Float) -> Float {
102104
t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2
103105
}
104106
}

Sources/LiveKitComponents/Components.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import LiveKit
18-
import SwiftUI
17+
@_exported import LiveKit
18+
@_exported import SwiftUI
1919

2020
public let liveKitComponentsVersion = "0.1.2"
2121

Sources/LiveKitComponents/Scopes/ComponentsScope.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
import LiveKit
1818
import SwiftUI
1919

20+
#if compiler(>=6.0)
21+
public extension EnvironmentValues {
22+
@Entry var liveKitUIOptions: UIOptions = .init()
23+
}
24+
#else
2025
private struct UIOptionsKey: EnvironmentKey {
2126
// This is the default value that SwiftUI will fallback to if you don't pass the object
2227
public static var defaultValue: UIOptions = .init()
@@ -28,6 +33,7 @@ public extension EnvironmentValues {
2833
set { self[UIOptionsKey.self] = newValue }
2934
}
3035
}
36+
#endif
3137

3238
public struct ComponentsScope<Content: View>: View {
3339
private let _content: () -> Content

Sources/LiveKitComponents/Types/TrackReference.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import LiveKit
1818
import SwiftUI
1919

20-
public class TrackReference: ObservableObject {
20+
@MainActor
21+
public final class TrackReference: ObservableObject {
2122
public let participant: Participant
2223
public let publication: TrackPublication?
2324
public let name: String?

Sources/LiveKitComponents/Types/UIOptions.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import LiveKit
1818
import SwiftUI
1919

2020
/// Subclass to customize default components UI.
21-
open class UIOptions: ObservableObject {
21+
open class UIOptions {
2222
// MARK: - Types
2323

2424
public enum TextFieldType {
@@ -37,6 +37,13 @@ open class UIOptions: ObservableObject {
3737

3838
public init() {}
3939

40+
/// Placeholder view when no track is enabled.
41+
open func noTrackView() -> AnyView {
42+
AnyView(
43+
EmptyView()
44+
)
45+
}
46+
4047
/// Placeholder view when the video is disabled or not available.
4148
open func videoDisabledView(geometry: GeometryProxy) -> AnyView {
4249
AnyView(

Sources/LiveKitComponents/UI/Participant/ParticipantView.swift

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,29 @@ public struct ParticipantView: View {
2828
}
2929

3030
public var body: some View {
31-
GeometryReader { geometry in
32-
ZStack(alignment: .topLeading) {
33-
let cameraReference = TrackReference(participant: _participant, source: .camera)
34-
let microphoneReference = TrackReference(participant: _participant, source: .microphone)
31+
ZStack(alignment: .topLeading) {
32+
let cameraReference = TrackReference(participant: _participant.avatarWorker ?? _participant, source: .camera)
33+
let microphoneReference = TrackReference(participant: _participant.avatarWorker ?? _participant, source: .microphone)
3534

36-
if let cameraTrack = cameraReference.resolve(), !cameraTrack.isMuted {
37-
VideoTrackView(trackReference: cameraReference)
38-
} else {
39-
if let microphoneTrack = microphoneReference.resolve(), !microphoneTrack.isMuted, let audioTrack = microphoneTrack.track as? AudioTrack {
40-
BarAudioVisualizer(audioTrack: audioTrack)
41-
} else {
42-
_ui.videoDisabledView(geometry: geometry)
43-
}
44-
}
35+
if let cameraTrack = cameraReference.resolve(), !cameraTrack.isMuted {
36+
VideoTrackView(trackReference: cameraReference)
37+
} else if let microphoneTrack = microphoneReference.resolve(), !microphoneTrack.isMuted,
38+
let audioTrack = microphoneTrack.track as? AudioTrack
39+
{
40+
BarAudioVisualizer(audioTrack: audioTrack, agentState: _participant.agentState).id(_participant.agentState)
41+
} else {
42+
_ui.noTrackView()
43+
}
4544

46-
if _showInformation {
47-
ParticipantInformationView()
48-
.padding(5)
49-
.background(Color.black.opacity(0.5))
50-
.cornerRadius(7)
51-
.padding()
52-
}
45+
if _showInformation {
46+
ParticipantInformationView()
47+
.padding(5)
48+
.background(Color.black.opacity(0.5))
49+
.cornerRadius(7)
50+
.padding()
5351
}
5452
}
53+
.animation(.easeOut, value: _participant.trackPublications)
54+
.frame(maxWidth: .infinity, maxHeight: .infinity)
5555
}
5656
}

0 commit comments

Comments
 (0)