Skip to content

Commit 0afa24c

Browse files
authored
Merge branch 'develop' into enhancement/improve-lobby-camera
2 parents 70122b8 + a88d70f commit 0afa24c

File tree

18 files changed

+362
-185
lines changed

18 files changed

+362
-185
lines changed

DemoApp/Sources/Components/Reactions/ReactionsAdapter.swift

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ final class ReactionsAdapter: ObservableObject, @unchecked Sendable {
1616

1717
var player: AVAudioPlayer?
1818

19-
private var reactionsTask: Task<Void, Error>?
2019
private var callEndedNotificationObserver: Any?
2120
private var activeCallUpdated: AnyCancellable?
21+
private let disposableBag = DisposableBag()
2222
private var call: Call? { didSet { subscribeToReactionEvents() } }
2323

2424
@Published var showFireworks = false
@@ -90,24 +90,22 @@ final class ReactionsAdapter: ObservableObject, @unchecked Sendable {
9090

9191
private func subscribeToReactionEvents() {
9292
guard let call else {
93-
reactionsTask?.cancel()
93+
disposableBag.removeAll()
9494
return
9595
}
9696

97-
let callReactionEventsStream = call.subscribe(for: CallReactionEvent.self)
98-
99-
reactionsTask = Task { [weak self] in
100-
for await event in callReactionEventsStream {
97+
call
98+
.eventPublisher(for: CallReactionEvent.self)
99+
.sinkTask(storeIn: disposableBag) { [weak self] event in
101100
guard
102101
let reaction = self?.reaction(for: event)
103102
else {
104-
continue
103+
return
105104
}
106105
self?.handleReaction(reaction, from: event.reaction.user.toUser)
107106
log.debug("\(event.reaction.user.name ?? event.reaction.user.id) reacted with reaction:\(reaction.id)")
108107
}
109-
return
110-
}
108+
.store(in: disposableBag)
111109
}
112110

113111
private func reaction(for event: CallReactionEvent) -> Reaction? {
@@ -206,12 +204,7 @@ final class ReactionsAdapter: ObservableObject, @unchecked Sendable {
206204
}
207205

208206
private func handleCallEnded() {
209-
Task {
210-
await MainActor.run { [weak self] in
211-
self?.reactionsTask?.cancel()
212-
self?.reactionsTask = nil
213-
}
214-
}
207+
disposableBag.removeAll()
215208
}
216209
}
217210

Sources/StreamVideo/Call.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,17 +1402,17 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
14021402
}
14031403
}
14041404

1405-
internal func onEvent(_ event: WrappedEvent) {
1405+
internal func onEvent(_ event: WrappedEvent) async {
14061406
guard case let .coordinatorEvent(videoEvent) = event else {
14071407
return
14081408
}
14091409
guard videoEvent.forCall(cid: cId) else {
14101410
return
14111411
}
1412-
executeOnMain { [weak self] in
1412+
await Task { @MainActor [weak self] in
14131413
guard let self else { return }
14141414
self.state.updateState(from: videoEvent)
1415-
}
1415+
}.value
14161416

14171417
eventSubject.send(event)
14181418
}

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
1414
@Injected(\.callCache) private var callCache
1515
@Injected(\.uuidFactory) private var uuidFactory
1616
@Injected(\.timers) private var timers
17+
private let disposableBag = DisposableBag()
1718

1819
/// Represents a call that is being managed by the service.
1920
final class CallEntry: Equatable, @unchecked Sendable {
@@ -90,7 +91,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
9091
private var active: UUID?
9192
var callCount: Int { storageAccessQueue.sync { _storage.count } }
9293

93-
private var callEventsSubscription: Task<Void, Error>?
9494
private var callEndedNotificationCancellable: AnyCancellable?
9595
private var ringingTimerCancellable: AnyCancellable?
9696

@@ -625,8 +625,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
625625
/// Subscribing to events is being used to reject/stop calls that have been accepted/rejected
626626
/// on other devices or components (e.g. incoming callScreen, CallKitService)
627627
private func subscribeToCallEvents() {
628-
callEventsSubscription?.cancel()
629-
callEventsSubscription = nil
628+
disposableBag.removeAll()
630629

631630
guard let streamVideo else {
632631
log.warning(
@@ -639,8 +638,10 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
639638
return
640639
}
641640

642-
callEventsSubscription = Task {
643-
for await event in streamVideo.subscribe() {
641+
streamVideo
642+
.eventPublisher()
643+
.sink { [weak self] event in
644+
guard let self else { return }
644645
switch event {
645646
case let .typeCallEndedEvent(response):
646647
callEnded(response.callCid, ringingTimedOut: false)
@@ -654,7 +655,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
654655
break
655656
}
656657
}
657-
}
658+
.store(in: disposableBag)
658659

659660
log.debug(
660661
"\(type(of: self)) is now subscribed to CallEvent updates.",

Sources/StreamVideo/Controllers/CallsController.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@ public class CallsController: ObservableObject, @unchecked Sendable {
3131

3232
private let callsQuery: CallsQuery
3333
private let streamVideo: StreamVideo
34-
35-
private var watchTask: Task<Void, Error>?
34+
3635
private var socketDisconnected = false
37-
private var cancellables = DisposableBag()
36+
private var disposableBag = DisposableBag()
3837

3938
init(streamVideo: StreamVideo, callsQuery: CallsQuery) {
4039
self.callsQuery = callsQuery
@@ -49,9 +48,7 @@ public class CallsController: ObservableObject, @unchecked Sendable {
4948
}
5049

5150
public func cleanUp() {
52-
watchTask?.cancel()
53-
watchTask = nil
54-
cancellables.removeAll()
51+
disposableBag.removeAll()
5552
}
5653

5754
// MARK: - private
@@ -67,7 +64,7 @@ public class CallsController: ObservableObject, @unchecked Sendable {
6764
self.reWatchCalls()
6865
}
6966
}
70-
.store(in: cancellables)
67+
.store(in: disposableBag)
7168
}
7269

7370
private func loadCalls(shouldRefresh: Bool = false) async throws {
@@ -190,11 +187,10 @@ public class CallsController: ObservableObject, @unchecked Sendable {
190187
}
191188

192189
private func subscribeToWatchEvents() {
193-
watchTask = Task {
194-
for await event in streamVideo.subscribe() {
195-
handle(event: event)
196-
}
197-
}
190+
streamVideo
191+
.eventPublisher()
192+
.sink { [weak self] in self?.handle(event: $0) }
193+
.store(in: disposableBag)
198194
}
199195

200196
deinit {

Sources/StreamVideo/StreamVideo.swift

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
1717
@Injected(\.callCache) private var callCache
1818
@Injected(\.timers) private var timers
1919

20+
private enum DisposableKey: String { case ringEventReceived }
21+
2022
public final class State: ObservableObject, @unchecked Sendable {
2123
@Published public internal(set) var connection: ConnectionStatus
2224
@Published public internal(set) var user: User
@@ -62,6 +64,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
6264
/// A protocol that provides a method to determine the rejection reason for a call.
6365
public lazy var rejectionReasonProvider: RejectionReasonProviding = StreamRejectionReasonProvider(self)
6466

67+
private let eventSubject: PassthroughSubject<WrappedEvent, Never> = .init()
68+
6569
var token: UserToken
6670

6771
private var tokenProvider: UserTokenProvider
@@ -78,7 +82,7 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
7882
private let eventsMiddleware = WSEventsMiddleware()
7983
private var cachedLocation: String?
8084
private var connectTask: Task<Void, Error>?
81-
private var eventHandlers = [EventHandler]()
85+
private let disposableBag = DisposableBag()
8286

8387
/// The notification center used to send and receive notifications about incoming events.
8488
private(set) lazy var eventNotificationCenter: EventNotificationCenter = {
@@ -219,6 +223,8 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
219223
if autoConnectOnInit {
220224
initialConnectIfRequired(apiKey: apiKey)
221225
}
226+
227+
observeCallRingEvents()
222228
}
223229

224230
deinit {
@@ -317,9 +323,6 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
317323

318324
/// Disconnects the current `StreamVideo` client.
319325
public func disconnect() async {
320-
eventHandlers.forEach { $0.cancel() }
321-
eventHandlers.removeAll()
322-
323326
await withCheckedContinuation { [webSocketClient] continuation in
324327
if let webSocketClient = webSocketClient {
325328
webSocketClient.disconnect {
@@ -330,35 +333,50 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
330333
}
331334
}
332335
}
333-
336+
337+
/// Publishes all received video events coming from the coordinator.
338+
///
339+
/// Use this method to observe all incoming `VideoEvent`s regardless of
340+
/// specific type. Events are filtered to only include those classified as
341+
/// `coordinatorEvent` cases.
342+
///
343+
/// - Returns: A publisher emitting `VideoEvent` instances.
344+
public func eventPublisher() -> AnyPublisher<VideoEvent, Never> {
345+
eventSubject
346+
.compactMap {
347+
guard case let .coordinatorEvent(event) = $0 else {
348+
return nil
349+
}
350+
return event
351+
}
352+
.eraseToAnyPublisher()
353+
}
354+
355+
/// Publishes specific typed WebSocket events.
356+
///
357+
/// Use this method to subscribe only to a specific type of event emitted by
358+
/// the coordinator. The `WSEvent` must conform to `Event`.
359+
///
360+
/// - Parameter event: The type of WebSocket event to observe.
361+
/// - Returns: A publisher emitting events of the specified `WSEvent` type.
362+
public func eventPublisher<WSEvent: Event>(
363+
for event: WSEvent.Type
364+
) -> AnyPublisher<WSEvent, Never> {
365+
eventSubject
366+
.compactMap { $0.unwrap()?.rawValue as? WSEvent }
367+
.eraseToAnyPublisher()
368+
}
369+
334370
/// Subscribes to all video events.
335371
/// - Returns: `AsyncStream` of `VideoEvent`s.
336372
public func subscribe() -> AsyncStream<VideoEvent> {
337-
AsyncStream(VideoEvent.self) { [weak self] continuation in
338-
let eventHandler = EventHandler(handler: { event in
339-
guard case let .coordinatorEvent(event) = event else {
340-
return
341-
}
342-
continuation.yield(event)
343-
}, cancel: { continuation.finish() })
344-
self?.eventHandlers.append(eventHandler)
345-
}
373+
eventPublisher().eraseAsAsyncStream()
346374
}
347375

348376
/// Subscribes to a particular WS event.
349377
/// - Returns: `AsyncStream` of the requested WS event.
350378
public func subscribe<WSEvent: Event>(for event: WSEvent.Type) -> AsyncStream<WSEvent> {
351-
AsyncStream(event) { [weak self] continuation in
352-
let eventHandler = EventHandler(handler: { event in
353-
guard let coordinatorEvent = event.unwrap() else {
354-
return
355-
}
356-
if let event = coordinatorEvent.unwrap() as? WSEvent {
357-
continuation.yield(event)
358-
}
359-
}, cancel: { continuation.finish() })
360-
self?.eventHandlers.append(eventHandler)
361-
}
379+
eventPublisher(for: event).eraseAsAsyncStream()
362380
}
363381

364382
public func queryCalls(
@@ -544,12 +562,6 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
544562
|| webSocketClient?.connectionState == .authenticating else {
545563
return ""
546564
}
547-
548-
var timeout = false
549-
let control = DefaultTimer.schedule(timeInterval: 5, queue: .sdk) {
550-
timeout = true
551-
}
552-
log.debug("Waiting for connection id")
553565

554566
do {
555567
return try await timers
@@ -728,24 +740,55 @@ extension StreamVideo: ConnectionStateDelegate {
728740
connectionRecoveryHandler?.webSocketClient(client, didUpdateConnectionState: state)
729741
}
730742
}
731-
eventHandlers.forEach { $0.handler(.internalEvent(WSDisconnected())) }
743+
eventSubject.send(.internalEvent(WSDisconnected()))
732744
case .connected(healthCheckInfo: _):
733-
eventHandlers.forEach { $0.handler(.internalEvent(WSConnected())) }
745+
eventSubject.send(.internalEvent(WSConnected()))
734746
default:
735747
log.debug("Web socket connection state update \(state)")
736748
}
737749
}
750+
751+
/// Observes incoming call ring events from the coordinator.
752+
///
753+
/// This method subscribes to `typeCallRingEvent` messages from the internal
754+
/// event stream. When such an event is received, it attempts to retrieve or
755+
/// create a `Call` object matching the event's call ID and type. Once the
756+
/// call is found, it updates the call's state with the event data and sets it
757+
/// as the current `ringingCall`.
758+
///
759+
/// The resulting subscription is stored in `disposableBag` under a specific
760+
/// key to allow later cancellation or cleanup.
761+
private func observeCallRingEvents() {
762+
eventSubject
763+
.eraseToAnyPublisher()
764+
.compactMap { (source: WrappedEvent) -> CallRingEvent? in
765+
guard
766+
case let .typeCallRingEvent(event) = source.unwrap()
767+
else {
768+
return nil
769+
}
770+
return event
771+
}
772+
.compactMap { [weak self] (source: CallRingEvent) -> (event: CallRingEvent, call: Call)? in
773+
guard let call = self?.call(callType: source.call.type, callId: source.call.id) else {
774+
return nil
775+
}
776+
return (event: source, call: call)
777+
}
778+
.sinkTask(storeIn: disposableBag) { @MainActor [weak self] in
779+
guard let self else { return }
780+
$0.call.state.update(from: $0.event)
781+
self.state.ringingCall = $0.call
782+
}
783+
.store(in: disposableBag, key: DisposableKey.ringEventReceived.rawValue)
784+
}
738785
}
739786

740787
extension StreamVideo: WSEventsSubscriber {
741788

742-
func onEvent(_ event: WrappedEvent) {
743-
for eventHandler in eventHandlers {
744-
eventHandler.handler(event)
745-
}
746-
Task { @MainActor [weak self] in
747-
self?.checkRingEvent(event)
748-
}
789+
func onEvent(_ event: WrappedEvent) async {
790+
eventSubject.send(event)
791+
checkRingEvent(event)
749792
}
750793

751794
private func checkRingEvent(_ event: WrappedEvent) {

Sources/StreamVideo/Utils/ClosedCaptionsAdapter/ClosedCaptionsAdapter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ final class ClosedCaptionsAdapter {
7474
- Parameter call: The call instance to subscribe for closed caption events.
7575
*/
7676
private func configure(with call: Call) {
77-
cancellable = AsyncStreamPublisher(call.subscribe(for: ClosedCaptionEvent.self))
77+
cancellable = call
78+
.eventPublisher(for: ClosedCaptionEvent.self)
7879
.map(\.closedCaption)
7980
.removeDuplicates()
8081
.log(.debug) { "Processing closedCaption for speakerId:\($0.speakerId) text:\($0.text)." }

Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalVideoMediaAdapter.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,6 @@ final class LocalVideoMediaAdapter: LocalMediaAdapting, @unchecked Sendable {
516516
}
517517
}
518518

519-
@MainActor
520519
private func adaptCaptureDimensions(
521520
for layerSettings: [Stream_Video_Sfu_Event_VideoSender]
522521
) async {

0 commit comments

Comments
 (0)