Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### πŸ”„ Changed

# [1.38.1](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.1)
_December 15, 2025_

### βœ… Added
- Configuration in `CallKitAdapter` to skip calls from showing in the `Recents` app. [#1008](https://github.com/GetStream/stream-video-swift/pull/1008)

### 🐞 Fixed
- An issue causing the local participant waveform to activate while the local participant wasn't speaking. [#1009](https://github.com/GetStream/stream-video-swift/pull/1009)

# [1.38.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.0)
_December 09, 2025_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ private func content() {
callKitAdapter.availabilityPolicy = .custom(MyCustomAvailabilityPolicy())
}

container {
@Injected(\.callKitAdapter) var callKitAdapter
callKitAdapter.includesCallsInRecents = false
}

container {
class IntentHandler: INExtension, INStartCallIntentHandling {
override func handler(for intent: INIntent) -> Any {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<a href="https://swift.org"><img src="https://img.shields.io/badge/Swift-5.9%2B-orange.svg" /></a>
</p>
<p align="center">
<img id="stream-video-label" alt="StreamVideo" src="https://img.shields.io/badge/StreamVideo-8.98%20MB-blue"/>
<img id="stream-video-label" alt="StreamVideo" src="https://img.shields.io/badge/StreamVideo-9.0%20MB-blue"/>
<img id="stream-video-swiftui-label" alt="StreamVideoSwiftUI" src="https://img.shields.io/badge/StreamVideoSwiftUI-2.38%20MB-blue"/>
<img id="stream-video-uikit-label" alt="StreamVideoUIKit" src="https://img.shields.io/badge/StreamVideoUIKit-2.5%20MB-blue"/>
<img id="stream-web-rtc-label" alt="StreamWebRTC" src="https://img.shields.io/badge/StreamWebRTC-11.02%20MB-blue"/>
Expand Down
6 changes: 6 additions & 0 deletions Sources/StreamVideo/CallKit/CallKitAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ open class CallKitAdapter {
set { callKitService.ringtoneSound = newValue }
}

/// Configure whether calls should appear in the Recents app.
open var includesCallsInRecents: Bool {
get { callKitService.includesCallsInRecents }
set { callKitService.includesCallsInRecents = newValue }
}

/// The callSettings to use when joining a call (after accepting it on CallKit)
/// default: nil
open var callSettings: CallSettings? {
Expand Down
3 changes: 3 additions & 0 deletions Sources/StreamVideo/CallKit/CallKitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// Whether video is supported. If true, CallKit push titles add "Video";
/// otherwise "Audio". Default is `false`.
open var supportsVideo: Bool = false
/// Whether calls received will be showing in Recents app.
open var includesCallsInRecents: Bool = true

/// Policy for handling calls when mic permission is missing while the app
/// runs in the background. See `CallKitMissingPermissionPolicy`.
Expand Down Expand Up @@ -692,6 +694,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
configuration.supportedHandleTypes = supportedHandleTypes
configuration.iconTemplateImageData = iconTemplateImageData
configuration.ringtoneSound = ringtoneSound
configuration.includesCallsInRecents = includesCallsInRecents

if supportsHolding {
// Holding a call isn't supported yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation

extension SystemEnvironment {
/// A Stream Video version.
public static let version: String = "1.38.0"
public static let version: String = "1.38.1"
/// The WebRTC version.
public static let webRTCVersion: String = "137.0.54"
}
2 changes: 1 addition & 1 deletion Sources/StreamVideo/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.38.1</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,6 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable

audioLevelsAdapter.subject = audioLevelSubject
source.observer = self

source.isVoiceProcessingBypassed = true
}

// MARK: - Recording
Expand All @@ -242,6 +240,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
/// sendAudio capability.
_ = source.setRecordingAlwaysPreparedMode(false)
source.prefersStereoPlayout = isPreferred
source.isVoiceProcessingBypassed = isPreferred
}

/// Starts or stops speaker playout on the ADM, retrying transient failures.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import StreamWebRTC

/// Bridges `RTCAudioCustomProcessingDelegate` callbacks into a Combine stream.
///
/// The delegate must stay extremely light-weight because WebRTC calls it while
/// holding an internal lock; any expensive work risks exceeding the ~10β€―ms
/// cadence and the native adapter will start skipping frames entirely. By
/// immediately forwarding each callback into a Combine publisher we decouple
/// the real-time audio path from the rest of the SDK: reducers, middleware, and
/// filters can subscribe (and hop to their own schedulers if needed) without
/// lengthening the time spent inside the delegate. This keeps capture responsive
/// while still letting higher layers observe every event and apply effects.
///
/// The module publishes structured events that middleware can observe to update
/// audio processing configuration and feed buffers into active filters.

Expand All @@ -27,6 +36,9 @@ final class AudioCustomProcessingModule: NSObject, RTCAudioCustomProcessingDeleg
/// Event stream used by store middleware to react to audio callbacks.
var publisher: AnyPublisher<Event, Never> { subject.eraseToAnyPublisher() }

/// Direct callback used for in-place filtering; must return within ~10β€―ms.
var processingHandler: ((RTCAudioBuffer) -> Void)?

/// RTCAudioCustomProcessingDelegate
func audioProcessingInitialize(
sampleRate sampleRateHz: Int,
Expand All @@ -42,6 +54,15 @@ final class AudioCustomProcessingModule: NSObject, RTCAudioCustomProcessingDeleg

/// RTCAudioCustomProcessingDelegate
func audioProcessingProcess(audioBuffer: RTCAudioBuffer) {
// Synchronous filtering happens via `processingHandler` so we can finish
// within WebRTC's ~10β€―ms processing budget; the native adapter wraps this
// delegate in a trylock and simply drops the frame if we block. Running
// Combine here would introduce scheduling/queue hops that blow the budget.
processingHandler?(audioBuffer)

// Downstream observers only need metadata (channel count, logs, etc.), so
// we enqueue the Combine event after the synchronous work; this publish is
// fire-and-forget and keeps the hot path lock-free.
subject.send(.audioProcessingProcess(audioBuffer))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,21 @@ extension AudioProcessingStore.Namespace {

final class AudioFilterMiddleware: Middleware<AudioProcessingStore.Namespace>, @unchecked Sendable {

private var cancellable: AnyCancellable?
@Injected(\.audioStore) private var audioStore

private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1)
private var currentRouteCancellable: AnyCancellable?

override init() {
super.init()
/// For some reason AudioFilters stop working after the audioOutput changes a couple of
/// times. Here we reapply the existing filter if any to ensure correct configuration
currentRouteCancellable = audioStore
.publisher(\.currentRoute)
.removeDuplicates()
.receive(on: processingQueue)
.sink { [weak self] _ in self?.reApplyFilterAfterRouteChange() }
}

override func apply(
state: AudioProcessingStore.Namespace.StoreState,
Expand All @@ -32,18 +46,21 @@ extension AudioProcessingStore.Namespace {
)
}
case let .setAudioFilter(audioFilter):
state.audioFilter?.release()
// Late filter selection: initialize if we already know format.
if state.initializedSampleRate > 0, state.initializedChannels > 0 {
audioFilter?.initialize(
sampleRate: state.initializedSampleRate,
channels: state.initializedChannels
processingQueue.addOperation { [weak self] in
guard let self else { return }
state.audioFilter?.release()
// Late filter selection: initialize if we already know format.
if state.initializedSampleRate > 0, state.initializedChannels > 0 {
audioFilter?.initialize(
sampleRate: state.initializedSampleRate,
channels: state.initializedChannels
)
}
didUpdate(
audioFilter,
capturePostProcessingDelegate: state.capturePostProcessingDelegate
)
}
didUpdate(
audioFilter,
capturePostProcessingDelegate: state.capturePostProcessingDelegate
)

case .release:
state.audioFilter?.release()
Expand All @@ -58,22 +75,15 @@ extension AudioProcessingStore.Namespace {
_ audioFilter: AudioFilter?,
capturePostProcessingDelegate: AudioCustomProcessingModule
) {
cancellable?.cancel()
cancellable = nil
capturePostProcessingDelegate.processingHandler = nil

guard let audioFilter else {
return
}

cancellable = capturePostProcessingDelegate
.publisher
.compactMap {
guard case let .audioProcessingProcess(buffer) = $0 else {
return nil
}
return buffer
}
.sink { [weak self, audioFilter] in self?.process($0, on: audioFilter) }
capturePostProcessingDelegate.processingHandler = { [weak self] in
self?.process($0, on: audioFilter)
}
}

private func process(
Expand All @@ -83,5 +93,20 @@ extension AudioProcessingStore.Namespace {
var audioBuffer = audioBuffer
audioFilter.applyEffect(to: &audioBuffer)
}

private func reApplyFilterAfterRouteChange() {
processingQueue.addOperation { [weak self] in
guard
let self,
let audioFilter = state?.audioFilter,
let capturePostProcessingDelegate = state?.capturePostProcessingDelegate
else {
return
}

didUpdate(nil, capturePostProcessingDelegate: capturePostProcessingDelegate)
didUpdate(audioFilter, capturePostProcessingDelegate: capturePostProcessingDelegate)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,21 @@ extension AVAudioSession {
self.renderingMode = ""
#endif

#if compiler(>=6.0)
#if compiler(>=6.1)
if #available(iOS 18.2, *) { self.prefersEchoCancelledInput = source.prefersEchoCancelledInput
} else { self.prefersEchoCancelledInput = false }
#else
self.prefersEchoCancelledInput = false
#endif

#if compiler(>=6.0)
#if compiler(>=6.1)
if #available(iOS 18.2, *) { self.isEchoCancelledInputEnabled = source.isEchoCancelledInputEnabled
} else { self.isEchoCancelledInputEnabled = false }
#else
self.isEchoCancelledInputEnabled = false
#endif

#if compiler(>=6.0)
#if compiler(>=6.1)
if #available(iOS 18.2, *) { self.isEchoCancelledInputAvailable = source.isEchoCancelledInputAvailable
} else { self.isEchoCancelledInputAvailable = false }
#else
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamVideoSwiftUI/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.38.1</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamVideoUIKit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.38.1</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
Expand Down
2 changes: 1 addition & 1 deletion StreamVideo-XCFramework.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamVideo-XCFramework'
spec.version = '1.38.0'
spec.version = '1.38.1'
spec.summary = 'StreamVideo iOS Video Client'
spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.'

Expand Down
2 changes: 1 addition & 1 deletion StreamVideo.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamVideo'
spec.version = '1.38.0'
spec.version = '1.38.1'
spec.summary = 'StreamVideo iOS Video Client'
spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.'

Expand Down
Loading