Skip to content

Commit f0064c4

Browse files
Merge branch 'develop' into enhancement/increase-spm-swift-version
2 parents 8e456e9 + 3b136e1 commit f0064c4

File tree

28 files changed

+1295
-50
lines changed

28 files changed

+1295
-50
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

55
# Upcoming
66

7-
### 🔄 Changed
7+
### ✅ Added
8+
- You can now configure policies based on the the device's proximity information. Those policies can be used to toggle speaker and video. [#770](https://github.com/GetStream/stream-video-swift/pull/770)
9+
10+
### 🐞 Fixed
11+
- Fix ringing flow issues. [#792](https://github.com/GetStream/stream-video-swift/pull/792)
812

913
# [1.21.1](https://github.com/GetStream/stream-video-swift/releases/tag/1.21.1)
1014
_April 25, 2025_

DemoApp/Sources/Components/AppEnvironment.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,35 @@ extension AppEnvironment {
591591
static var preferredCallType: String?
592592
}
593593

594+
extension AppEnvironment {
595+
596+
enum ProximityPolicyDebugConfiguration: Hashable, Debuggable, Sendable, CaseIterable {
597+
case speaker, video
598+
599+
var title: String {
600+
switch self {
601+
case .speaker:
602+
return "Speaker"
603+
case .video:
604+
return "Video"
605+
}
606+
}
607+
608+
var value: ProximityPolicy {
609+
switch self {
610+
case .speaker:
611+
return SpeakerProximityPolicy()
612+
case .video:
613+
return VideoProximityPolicy()
614+
}
615+
}
616+
}
617+
618+
static var proximityPolicies: Set<ProximityPolicyDebugConfiguration> = {
619+
[.speaker, .video]
620+
}()
621+
}
622+
594623
extension String: Debuggable {
595624
var title: String {
596625
self

DemoApp/Sources/Views/CallView/CallingView/SimpleCallingView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ struct SimpleCallingView: View {
188188
try await call.updateAudioSessionPolicy(AppEnvironment.audioSessionPolicy.value)
189189
}
190190

191+
private func setProximityPolicies(for callId: String) throws {
192+
let policies = AppEnvironment.proximityPolicies.map(\.value)
193+
guard !policies.isEmpty else {
194+
return
195+
}
196+
let call = streamVideo.call(callType: callType, callId: callId)
197+
try policies.forEach { try call.addProximityPolicy($0) }
198+
}
199+
191200
private func parseURLIfRequired(_ text: String) {
192201
let adapter = DeeplinkAdapter()
193202
guard
@@ -224,6 +233,7 @@ struct SimpleCallingView: View {
224233
case .lobby:
225234
await setPreferredVideoCodec(for: text)
226235
try? await setAudioSessionPolicyOverride(for: text)
236+
try? setProximityPolicies(for: text)
227237
viewModel.enterLobby(
228238
callType: callType,
229239
callId: text,
@@ -232,10 +242,12 @@ struct SimpleCallingView: View {
232242
case .join:
233243
await setPreferredVideoCodec(for: text)
234244
try? await setAudioSessionPolicyOverride(for: text)
245+
try? setProximityPolicies(for: text)
235246
viewModel.joinCall(callType: callType, callId: text)
236247
case let .start(callId):
237248
await setPreferredVideoCodec(for: callId)
238249
try? await setAudioSessionPolicyOverride(for: callId)
250+
try? setProximityPolicies(for: text)
239251
viewModel.startCall(
240252
callType: callType,
241253
callId: callId,

DemoApp/Sources/Views/Login/DebugMenu.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ struct DebugMenu: View {
125125
didSet { AppEnvironment.audioSessionPolicy = audioSessionPolicy }
126126
}
127127

128+
@State private var proximityPolicies = AppEnvironment.proximityPolicies {
129+
didSet { AppEnvironment.proximityPolicies = proximityPolicies }
130+
}
131+
128132
var body: some View {
129133
Menu {
130134
makeMenu(
@@ -190,6 +194,18 @@ struct DebugMenu: View {
190194
label: "AudioSession policy"
191195
) { self.audioSessionPolicy = $0 }
192196

197+
makeMultipleSelectMenu(
198+
for: AppEnvironment.ProximityPolicyDebugConfiguration.allCases,
199+
currentValues: proximityPolicies,
200+
label: "Proximity policies"
201+
) { item, isSelected in
202+
if isSelected {
203+
proximityPolicies = proximityPolicies.filter { item != $0 }
204+
} else {
205+
proximityPolicies.insert(item)
206+
}
207+
}
208+
193209
makeMenu(
194210
for: [.default, .lastParticipant],
195211
currentValue: autoLeavePolicy,

DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
409C396F2B67CDF30090044C /* 09-broadcasting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C396E2B67CDF30090044C /* 09-broadcasting.swift */; };
2929
409F10EA2BE11698000A984B /* StreamVideoNoiseCancellation in Frameworks */ = {isa = PBXBuildFile; productRef = 409F10E92BE11698000A984B /* StreamVideoNoiseCancellation */; };
3030
40AB356B2B73965D00E465CC /* 16-snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB356A2B73965D00E465CC /* 16-snapshot.swift */; };
31+
40AD64BB2DC2671E0077AE15 /* 05-proximity-policies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AD64BA2DC2671B0077AE15 /* 05-proximity-policies.swift */; };
3132
40B468982B67B6DF009B5B3E /* 01-deeplinking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B468972B67B6DF009B5B3E /* 01-deeplinking.swift */; };
3233
40CB9FA92B7FC7DB006BED93 /* 17-camera-zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CB9FA82B7FC7DB006BED93 /* 17-camera-zoom.swift */; };
3334
40E23B9B2B67BA4100D11FC1 /* 02-push-notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E23B9A2B67BA4100D11FC1 /* 02-push-notifications.swift */; };
@@ -108,6 +109,7 @@
108109
409C396C2B67CD780090044C /* 08-recording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-recording.swift"; sourceTree = "<group>"; };
109110
409C396E2B67CDF30090044C /* 09-broadcasting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "09-broadcasting.swift"; sourceTree = "<group>"; };
110111
40AB356A2B73965D00E465CC /* 16-snapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "16-snapshot.swift"; sourceTree = "<group>"; };
112+
40AD64BA2DC2671B0077AE15 /* 05-proximity-policies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-proximity-policies.swift"; sourceTree = "<group>"; };
111113
40B468972B67B6DF009B5B3E /* 01-deeplinking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-deeplinking.swift"; sourceTree = "<group>"; };
112114
40CB9FA82B7FC7DB006BED93 /* 17-camera-zoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "17-camera-zoom.swift"; sourceTree = "<group>"; };
113115
40E23B9A2B67BA4100D11FC1 /* 02-push-notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-push-notifications.swift"; sourceTree = "<group>"; };
@@ -298,6 +300,7 @@
298300
409C39682B67CC5C0090044C /* 04-screensharing.swift */,
299301
409C396A2B67CD0B0090044C /* 05-picture-in-picture.swift */,
300302
404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */,
303+
40AD64BA2DC2671B0077AE15 /* 05-proximity-policies.swift */,
301304
409C396C2B67CD780090044C /* 08-recording.swift */,
302305
409C396E2B67CDF30090044C /* 09-broadcasting.swift */,
303306
);
@@ -440,6 +443,7 @@
440443
files = (
441444
40FFDC5A2B63F08D004DA7A2 /* 01-call-participant.swift in Sources */,
442445
40FFDC9A2B6405C7004DA7A2 /* 10-network-quality-indicator.swift in Sources */,
446+
40AD64BB2DC2671E0077AE15 /* 05-proximity-policies.swift in Sources */,
443447
400D91D12B63DEA200EBA47D /* 04-camera-and-microphone.swift in Sources */,
444448
40FFDC4F2B63EE50004DA7A2 /* 04-active-call.swift in Sources */,
445449
40FFDCA02B640676004DA7A2 /* 13-pinning-users.swift in Sources */,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// 05-proximity-policies.swift
3+
// DocumentationTests
4+
//
5+
// Created by Ilias Pavlidakis on 30/4/25.
6+
//
7+
8+
import StreamVideo
9+
import StreamVideoSwiftUI
10+
import SwiftUI
11+
import Combine
12+
13+
@MainActor
14+
fileprivate func content() {
15+
container {
16+
let call = streamVideo.call(callType: "default", callId: "chat-123")
17+
let policy = VideoProximityPolicy()
18+
19+
do {
20+
try call.addProximityPolicy(policy)
21+
} catch {
22+
log.error(error)
23+
}
24+
}
25+
26+
container {
27+
let call = streamVideo.call(callType: "default", callId: "team-meet")
28+
let policy = SpeakerProximityPolicy()
29+
30+
do {
31+
try call.addProximityPolicy(policy)
32+
} catch {
33+
log.error(error)
34+
}
35+
}
36+
37+
container {
38+
let call = streamVideo.call(callType: "default", callId: "chat-123")
39+
let videoPolicy = VideoProximityPolicy()
40+
let speakerPolicy = SpeakerProximityPolicy()
41+
42+
do {
43+
try call.addProximityPolicy(videoPolicy)
44+
try call.addProximityPolicy(speakerPolicy)
45+
} catch {
46+
log.error(error)
47+
}
48+
}
49+
}

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ GEM
210210
bundler
211211
fastlane
212212
pry
213-
fastlane-plugin-stream_actions (0.3.78)
213+
fastlane-plugin-stream_actions (0.3.79)
214214
xctest_list (= 1.2.1)
215215
fastlane-plugin-versioning (0.7.1)
216216
fastlane-sirp (1.0.0)
@@ -443,7 +443,7 @@ DEPENDENCIES
443443
fastlane
444444
fastlane-plugin-create_xcframework
445445
fastlane-plugin-lizard
446-
fastlane-plugin-stream_actions (= 0.3.78)
446+
fastlane-plugin-stream_actions (= 0.3.79)
447447
fastlane-plugin-versioning
448448
jazzy
449449
json

Sources/StreamVideo/Call.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
4848
public let camera: CameraManager
4949
/// Provides access to the speaker.
5050
public let speaker: SpeakerManager
51+
/// Provides access to device's proximity
52+
private lazy var proximity: ProximityManager = .init(self)
5153

5254
internal let callController: CallController
5355
internal let coordinatorClient: DefaultAPI
@@ -1232,7 +1234,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
12321234
/// Sets the disconnection timeout for a user who has temporarily lost connection.
12331235
///
12341236
/// This method defines the duration a user, who has already joined the call, can remain
1235-
/// in a disconnected state due to temporary internet issues. If the users connection
1237+
/// in a disconnected state due to temporary internet issues. If the user's connection
12361238
/// remains disrupted beyond the specified timeout period, they will be dropped from the call.
12371239
/// This timeout helps ensure that users with unstable connections do not stay in the call
12381240
/// indefinitely if they cannot reconnect.
@@ -1367,6 +1369,20 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
13671369
try await callController.updateAudioSessionPolicy(policy)
13681370
}
13691371

1372+
/// Adds a proximity policy to manage device proximity behavior during the call.
1373+
/// Only supported on phone devices.
1374+
/// - Parameter policy: Policy to add for managing proximity behavior
1375+
/// - Throws: ClientError if the call is not in a valid state
1376+
public func addProximityPolicy(_ policy: any ProximityPolicy) throws {
1377+
try proximity.add(policy)
1378+
}
1379+
1380+
/// Removes a previously added proximity policy from the call.
1381+
/// - Parameter policy: Policy to remove
1382+
public func removeProximityPolicy(_ policy: any ProximityPolicy) {
1383+
proximity.remove(policy)
1384+
}
1385+
13701386
// MARK: - Internal
13711387

13721388
internal func update(reconnectionStatus: ReconnectionStatus) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Combine
6+
import Foundation
7+
8+
/// Manages proximity-related functionality for a call, including policy management
9+
/// and proximity state observation. Only active on phone devices.
10+
final class ProximityManager: @unchecked Sendable {
11+
12+
@Injected(\.streamVideo) private var streamVideo
13+
@Injected(\.currentDevice) private var currentDevice
14+
@Injected(\.proximityMonitor) private var proximityMonitor
15+
16+
/// Whether proximity monitoring is supported on the current device
17+
var isSupported: Bool { currentDevice.deviceType == .phone }
18+
19+
/// Weak reference to the associated call
20+
private weak var call: Call?
21+
/// Thread-safe storage for registered proximity policies
22+
@Atomic private var policies: [ObjectIdentifier: any ProximityPolicy] = [:]
23+
24+
/// Cancellable for active call observation
25+
private var activeCallCancellable: AnyCancellable?
26+
/// Cancellable for proximity state observation
27+
private var observationCancellable: AnyCancellable?
28+
29+
/// Creates a new proximity manager for the specified call
30+
/// - Parameter call: Call instance to manage proximity for
31+
init(_ call: Call) {
32+
self.call = call
33+
34+
if isSupported {
35+
activeCallCancellable = streamVideo
36+
.state
37+
.$activeCall
38+
.sinkTask { @MainActor [weak self] in self?.didUpdateActiveCall($0) }
39+
}
40+
}
41+
42+
deinit {
43+
activeCallCancellable?.cancel()
44+
observationCancellable?.cancel()
45+
}
46+
47+
// MARK: - Policies
48+
49+
/// Adds a proximity policy to be managed by this instance
50+
/// - Parameter policy: Policy to add
51+
/// - Throws: ClientError if call is nil
52+
func add(_ policy: any ProximityPolicy) throws {
53+
guard isSupported else { return }
54+
55+
guard let call else {
56+
throw ClientError("ProximityPolicy identifier:\(policy) cannot be attached while Call is nil.")
57+
}
58+
policies[type(of: policy).identifier] = policy
59+
log.info("ProximityPolicy identifier:\(policy) has been attached on Call id:\(call.callId) type:\(call.callType).")
60+
}
61+
62+
/// Removes a proximity policy from management
63+
/// - Parameter policy: Policy to remove
64+
func remove(_ policy: any ProximityPolicy) {
65+
guard isSupported else { return }
66+
67+
policies[type(of: policy).identifier] = nil
68+
69+
guard let call else {
70+
return
71+
}
72+
log.info("ProximityPolicy identifier:\(policy) has been removed from Call id:\(call.callId) type:\(call.callType).")
73+
}
74+
75+
// MARK: - Private Helpers
76+
77+
/// Handles active call changes by starting/stopping proximity observation
78+
/// - Parameter activeCall: New active call or nil if no active call
79+
@MainActor
80+
private func didUpdateActiveCall(_ activeCall: Call?) {
81+
if
82+
let activeCall,
83+
call?.cId == activeCall.cId,
84+
policies.isEmpty == false,
85+
observationCancellable == nil {
86+
proximityMonitor.startObservation()
87+
observationCancellable = proximityMonitor
88+
.statePublisher
89+
.removeDuplicates()
90+
.sink { [weak self] in self?.didUpdateProximity($0) }
91+
log.info("Proximity observation has started.")
92+
} else if
93+
activeCall == nil,
94+
observationCancellable != nil
95+
{
96+
observationCancellable?.cancel()
97+
observationCancellable = nil
98+
proximityMonitor.stopObservation()
99+
log.info("Proximity observation has stopped.")
100+
} else {
101+
/* No-op */
102+
}
103+
}
104+
105+
/// Notifies all registered policies of a proximity state change
106+
/// - Parameter proximity: New proximity state
107+
private func didUpdateProximity(_ proximity: ProximityState) {
108+
guard let call else {
109+
return
110+
}
111+
112+
for policy in policies {
113+
policy.value.didUpdateProximity(proximity, on: call)
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)