Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
### ✅ Added
- `ClientCapabilities` have been added to support remote subscriber track pause. [#888](https://github.com/GetStream/stream-video-swift/pull/888)

# [1.28.1](https://github.com/GetStream/stream-video-swift/releases/tag/1.28.1)
_July 11, 2025_
Expand Down
14 changes: 14 additions & 0 deletions DemoApp/Sources/Components/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,20 @@ extension AppEnvironment {
}()
}

extension AppEnvironment {

static var clientCapabilities: Set<ClientCapability>?
}

extension ClientCapability: Debuggable {
var title: String {
switch self {
case .subscriberVideoPause:
"Subscriber video pause"
}
}
}

extension String: Debuggable {
var title: String {
self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ struct SimpleCallingView: View {
try policies.forEach { try call.addProximityPolicy($0) }
}

private func setClientCapabilities(for callId: String) async {
guard let clientCapabilities = AppEnvironment.clientCapabilities else {
return
}
let call = streamVideo.call(callType: callType, callId: callId)
await call.updateClientCapabilities(clientCapabilities)
}

private func parseURLIfRequired(_ text: String) {
let adapter = DeeplinkAdapter()
guard
Expand Down Expand Up @@ -234,6 +242,7 @@ struct SimpleCallingView: View {
await setPreferredVideoCodec(for: text)
try? await setAudioSessionPolicyOverride(for: text)
try? setProximityPolicies(for: text)
await setClientCapabilities(for: text)
viewModel.enterLobby(
callType: callType,
callId: text,
Expand All @@ -243,11 +252,13 @@ struct SimpleCallingView: View {
await setPreferredVideoCodec(for: text)
try? await setAudioSessionPolicyOverride(for: text)
try? setProximityPolicies(for: text)
await setClientCapabilities(for: text)
viewModel.joinCall(callType: callType, callId: text)
case let .start(callId):
await setPreferredVideoCodec(for: callId)
try? await setAudioSessionPolicyOverride(for: callId)
try? setProximityPolicies(for: callId)
await setClientCapabilities(for: callId)
viewModel.startCall(
callType: callType,
callId: callId,
Expand Down
41 changes: 41 additions & 0 deletions DemoApp/Sources/Views/Login/DebugMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ struct DebugMenu: View {
didSet { AppEnvironment.proximityPolicies = proximityPolicies }
}

@State private var availableClientCapabilities = ClientCapability.allCases
@State private var preferredClientCapabilities = AppEnvironment.clientCapabilities {
didSet { AppEnvironment.clientCapabilities = preferredClientCapabilities }
}

var body: some View {
Menu {
makeMenu(
Expand Down Expand Up @@ -188,6 +193,40 @@ struct DebugMenu: View {
label: "ClosedCaptions Integration"
) { self.closedCaptionsIntegration = $0 }

makeMultipleSelectMenu(
for: availableClientCapabilities,
currentValues: preferredClientCapabilities ?? [],
additionalItems: {
if preferredClientCapabilities != nil {
Divider()

Button {
self.preferredClientCapabilities = nil
} label: {
Text("Remove overrides")
}
} else {
EmptyView()
}
},
label: "Override Client Capabilities"
) { item, isSelected in
if isSelected {
if let preferredClientCapabilities {
if preferredClientCapabilities.count == 1 {
self.preferredClientCapabilities = nil
} else {
self.preferredClientCapabilities = preferredClientCapabilities.filter { item != $0 }
}
}
} else {
if preferredClientCapabilities == nil {
preferredClientCapabilities = Set<ClientCapability>()
}
preferredClientCapabilities?.insert(item)
}
}

makeMenu(
for: [.default, .ownCapabilities],
currentValue: audioSessionPolicy,
Expand Down Expand Up @@ -484,6 +523,7 @@ struct DebugMenu: View {
private func makeMultipleSelectMenu<Item: Debuggable>(
for items: [Item],
currentValues: Set<Item>,
@ViewBuilder additionalItems: () -> some View = { EmptyView() },
label: String,
updater: @escaping (Item, Bool) -> Void
) -> some View {
Expand All @@ -501,6 +541,7 @@ struct DebugMenu: View {
}
}
}
additionalItems()
} label: {
Text(label)
}
Expand Down
3 changes: 2 additions & 1 deletion DemoApp/Sources/Views/Reactions/ReactionOverlayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ struct ReactionOverlayView_Previews: PreviewProvider {
joinedAt: Date(),
audioLevel: 0,
audioLevels: [],
pin: nil
pin: nil,
pausedTracks: []
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ struct ReactionsViewModifier_Previews: PreviewProvider {
joinedAt: Date(),
audioLevel: 0,
audioLevels: [],
pin: nil
pin: nil,
pausedTracks: []
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */; };
404CAED82B8E3874007087BC /* 06-apply-video-filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */; };
4068C1252B67C056006B0BEE /* 03-callkit-integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */; };
40895E622E264BB000D3049D /* 25-incoming-video-state.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40895E612E264BB000D3049D /* 25-incoming-video-state.swift */; };
408CE0F52BD91B490052EC3A /* 19-transcriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */; };
409774B02CC19F5500E0D3EE /* 23-network-disruption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */; };
409C39692B67CC5C0090044C /* 04-screensharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C39682B67CC5C0090044C /* 04-screensharing.swift */; };
Expand Down Expand Up @@ -102,6 +103,7 @@
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "22-manual-quality-selection.swift"; sourceTree = "<group>"; };
404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-apply-video-filters.swift"; sourceTree = "<group>"; };
4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-callkit-integration.swift"; sourceTree = "<group>"; };
40895E612E264BB000D3049D /* 25-incoming-video-state.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "25-incoming-video-state.swift"; sourceTree = "<group>"; };
408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "19-transcriptions.swift"; sourceTree = "<group>"; };
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "23-network-disruption.swift"; sourceTree = "<group>"; };
409C39682B67CC5C0090044C /* 04-screensharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-screensharing.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -286,6 +288,7 @@
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */,
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */,
401C1EF32D494CED00304609 /* 24-closed-captions.swift */,
40895E612E264BB000D3049D /* 25-incoming-video-state.swift */,
);
path = "05-ui-cookbook";
sourceTree = "<group>";
Expand Down Expand Up @@ -487,6 +490,7 @@
40FFDC442B63E95D004DA7A2 /* 14-swiftui-vs-uikit.swift in Sources */,
40FFDC872B63FEAE004DA7A2 /* 05-incoming-call.swift in Sources */,
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */,
40895E622E264BB000D3049D /* 25-incoming-video-state.swift in Sources */,
40FFDC942B6401CC004DA7A2 /* 07-video-fallback.swift in Sources */,
400D91C72B63D96800EBA47D /* 03-quickstart.swift in Sources */,
40FFDC9E2B64063D004DA7A2 /* 12-connection-unstable.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import StreamVideo
import StreamVideoSwiftUI
import SwiftUI
import Combine

@MainActor
fileprivate func content() {
asyncContainer {
let call = streamVideo.call(callType: "default", callId: "my-call-id")
await call.updateClientCapabilities([.subscriberVideoPause])
}

container {
let cancellable = call
.state
.$participants
.sink { participants in
let pausedVideoParticipants = participants.filter {
$0.pausedTracks.contains(.video)
}

print("Participants with paused video tracks: \(pausedVideoParticipants)")
}

// Cancel when no longer needed:
cancellable.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ var participant = CallParticipant(
joinedAt: .init(),
audioLevel: 0,
audioLevels: [],
pin: nil
pin: nil,
pausedTracks: []
)
var contentMode = UIView.ContentMode.scaleAspectFit
var id = ""
Expand Down Expand Up @@ -421,7 +422,8 @@ var otherParticipant = CallParticipant(
joinedAt: .init(),
audioLevel: 0,
audioLevels: [],
pin: nil
pin: nil,
pausedTracks: []
)

final class UserManager {
Expand Down
10 changes: 10 additions & 0 deletions Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,16 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
proximity.remove(policy)
}

// MARK: - ClientCapabilities

/// Updates the set of client capabilities for the call.
///
/// - Parameter clientCapabilities: A set of client capabilities that influence
/// subscription logic (e.g., support for paused tracks).
public func updateClientCapabilities(_ clientCapabilities: Set<ClientCapability>) async {
await callController.updateClientCapabilities(clientCapabilities)
}

// MARK: - Internal

internal func update(reconnectionStatus: ReconnectionStatus) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/StreamVideo/Controllers/CallController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ class CallController: @unchecked Sendable {
try webRTCCoordinator.callKitActivated(audioSession)
}

func updateClientCapabilities(_ capabilities: Set<ClientCapability>) async {
await webRTCCoordinator.updateClientCapabilities(capabilities)
}

// MARK: - private

private func handleParticipantsUpdated() {
Expand Down
Loading
Loading