Skip to content

Conversation

@ipavlidakis
Copy link
Contributor

@ipavlidakis ipavlidakis commented Oct 16, 2025

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-1103/hifi-implementation

🎯 Goal

Ship the “HiFi” audio experience by keeping stereo playout enabled for livestream calls while keeping CallKit and WebRTC session management stable.

📝 Summary

  • Add LivestreamAudioSessionPolicy so livestream calls request stereo-ready categories and speaker routing.
  • Extend CallAudioSession with a debounced processing pipeline that reacts to settings, capabilities, and route changes on a background queue.
  • Introduce a StereoPlayoutEffect and AVAudioSessionObserver to keep the WebRTC audio device module and store state in sync with real hardware changes.
  • Expand RTCAudioDeviceModuleControlling to surface stereo/mute publishers and expose refreshStereoPlayoutState(), wiring the updates into the store.

🛠 Implementation

  • The new policy selects .playAndRecord vs .playback, toggles allowBluetoothA2DP, and mirrors speaker preferences so livestream viewers keep stereo output without requiring extra toggles.
  • CallAudioSession now funnels every change through a serial OperationQueue, caching the last configuration and reapplying it only when a route change actually demands it. We also trace the resulting configuration for telemetry.
  • AVAudioSessionObserver snapshots the live session every 100 ms, exposing a Combine publisher that feeds into StereoPlayoutEffect; the effect watches the injected AudioDeviceModule, re-requests stereo if routes change silently, and dispatches enablement updates back into the store.
  • RTCAudioDeviceModuleControlling gains Combine publishers for mute/voice-processing flags and a concrete refreshStereoPlayoutState() so the effect can explicitly rescan hardware capability, ensuring WebRTC flips to stereo as soon as the underlying session allows it.

🎨 Showcase

Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.

Before After
img img

🧪 Manual Testing Notes

Explain how this change can be tested manually, if applicable.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

🎁 Meme

Provide a funny gif or image that relates to your work on this pull request. (Optional)

@ipavlidakis ipavlidakis self-assigned this Oct 16, 2025
@ipavlidakis ipavlidakis added the enhancement New feature or request label Oct 16, 2025
@ipavlidakis ipavlidakis changed the title Enhancement/hifi/feature implementation [Enhancement]HiFi feature implementation Oct 16, 2025
@ipavlidakis ipavlidakis force-pushed the enhancement/hifi/feature-implementation branch from 9484432 to 4561d95 Compare October 16, 2025 09:45
@github-actions
Copy link

github-actions bot commented Oct 16, 2025

2 Warnings
⚠️ Please be sure to complete the Contributor Checklist in the Pull Request description
⚠️ Big PR

Generated by 🚫 Danger

@ipavlidakis ipavlidakis force-pushed the enhancement/hifi/feature-implementation branch 5 times, most recently from eb2eb3b to 283a24a Compare October 20, 2025 13:50
@ipavlidakis ipavlidakis force-pushed the enhancement/hifi/feature-implementation branch from ad2ecd8 to ebfedd9 Compare November 11, 2025 14:51
@ipavlidakis ipavlidakis force-pushed the enhancement/hifi/feature-implementation branch from ebfedd9 to 6b6c886 Compare November 20, 2025 01:46
@ipavlidakis ipavlidakis marked this pull request as ready for review November 28, 2025 10:35
@ipavlidakis ipavlidakis requested a review from a team as a code owner November 28, 2025 10:35
@github-actions
Copy link

Public Interface

+ public struct LivestreamAudioSessionPolicy: AudioSessionPolicy  
+ 
+   public init()
+   
+ 
+   public func configuration(for callSettings: CallSettings,ownCapabilities: Set<OwnCapability>)-> AudioSessionConfiguration

+ public enum WebRTC  
+ 
+   case audioEngineDevice = "audio_engine_device.mm"
+   
+ 
+   public nonisolated static var mode: LogMode
+   
+ 
+   public enum LogMode  
+   
+     case none, validFilesOnly, all



 @MainActor public class CallState: ObservableObject  
-   @Published public internal var callSettings
+   @Published public internal var callSettings: CallSettings

 public final class CallSettings: ObservableObject, Sendable, Equatable, CustomStringConvertible  
-   public let audioOn: Bool
+   public static let `default`
-   public let videoOn: Bool
+   public let audioOn: Bool
-   public let speakerOn: Bool
+   public let videoOn: Bool
-   public let audioOutputOn: Bool
+   public let speakerOn: Bool
-   public let cameraPosition: CameraPosition
+   public let audioOutputOn: Bool
-   public var shouldPublish: Bool
+   public let cameraPosition: CameraPosition
-   public var description: String
+   public var shouldPublish: Bool
-   
+   public var description: String
- 
+   
-   public convenience init(_ response: CallSettingsResponse,file: StaticString = #file,function: StaticString = #function,line: UInt = #line)
+ 
-   public init(audioOn: Bool = true,videoOn: Bool = true,speakerOn: Bool = true,audioOutputOn: Bool = true,cameraPosition: CameraPosition = .front,file: StaticString = #file,function: StaticString = #function,line: UInt = #line)
+   public convenience init(_ response: CallSettingsResponse,file: StaticString = #file,function: StaticString = #function,line: UInt = #line)
-   
+   public init(audioOn: Bool = true,videoOn: Bool = true,speakerOn: Bool = true,audioOutputOn: Bool = true,cameraPosition: CameraPosition = .front,file: StaticString = #file,function: StaticString = #function,line: UInt = #line)
- 
+   
-   public static func ==(lhs: CallSettings,rhs: CallSettings)-> Bool
+ 
+   public static func ==(lhs: CallSettings,rhs: CallSettings)-> Bool

- public class ClientError: Error, ReflectiveStringConvertible, @unchecked Sendable  
+ public class ClientError: Error, CustomStringConvertible, @unchecked Sendable  
-   
+   public var description: String
- 
+   
-   public init(with error: Error? = nil,_ file: StaticString = #fileID,_ line: UInt = #line)
+ 
-   public init(_ message: String,_ file: StaticString = #fileID,_ line: UInt = #line)
+   public init(with error: Error? = nil,_ file: StaticString = #fileID,_ line: UInt = #line)
-   
+   public init(_ message: String,_ file: StaticString = #fileID,_ line: UInt = #line)
- 
+   
-   public struct Location: Equatable, Sendable  
+ 
-   
+   public struct Location: Equatable, Sendable, CustomStringConvertible  
-     public let file: String
+   
-     public let line: Int
+     public let file: String
+     public let line: Int
+     public var description: String

 public struct StatelessMicrophoneIconView: View  
-   @MainActor public init(call: Call?,callSettings: CallSettings = .init(),size: CGFloat = 44,controlStyle: ToggleControlStyle = .init(
+   @MainActor public init(call: Call?,callSettings: CallSettings = .default,size: CGFloat = 44,controlStyle: ToggleControlStyle = .init(

- public struct AudioSessionConfiguration: ReflectiveStringConvertible, Equatable, Sendable  
+ public struct AudioSessionConfiguration: CustomStringConvertible, Equatable, Sendable  
-   public static func ==(lhs: Self,rhs: Self)-> Bool
+   public var description: String
+   
+ 
+   public static func ==(lhs: Self,rhs: Self)-> Bool

 public struct StatelessVideoIconView: View  
-   public init(call: Call?,callSettings: CallSettings = .init(),size: CGFloat = 44,controlStyle: ToggleControlStyle = .init(
+   public init(call: Call?,callSettings: CallSettings = .default,size: CGFloat = 44,controlStyle: ToggleControlStyle = .init(

 public final class MicrophoneManager: ObservableObject, CallSettingsManager, @unchecked Sendable  
-   public func toggle()async throws 
+   public func toggle(file: StaticString = #file,function: StaticString = #function,line: UInt = #line)async throws 
-   public func enable()async throws 
+   public func enable(file: StaticString = #file,function: StaticString = #function,line: UInt = #line)async throws 
-   public func disable()async throws
+   public func disable(file: StaticString = #file,function: StaticString = #function,line: UInt = #line)async throws

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title develop branch diff status
StreamVideo 8.79 MB 8.98 MB +196 KB 🟢
StreamVideoSwiftUI 2.4 MB 2.4 MB 0 KB 🟢
StreamVideoUIKit 2.52 MB 2.52 MB 0 KB 🟢
StreamWebRTC 11.01 MB 11.02 MB +17 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator

StreamVideo XCSize

Object Diff (bytes)
Errors.o +48183
SignalServerEvent.o -46530
RTCAudioStore+State.o +36763
AudioDeviceModule.o +26852
BatteryStore.o +16827
Show 104 more objects
Object Diff (bytes)
RTCAudioStore+Action.o +15269
ApplicationLifecycleVideoMuteAdapter.o -13118
RTCAudioStore.o -12881
StreamCallAudioRecorder.o +11607
CallParticipant.o +11138
AVAudioSessionObserver.o +9503
DisposableBag.o +9209
Logger+WebRTC.o +8726
RTCAudioStore+AVAudioSessionReducer.o +8501
RTCAudioStore+StereoPlayoutEffect.o +7355
RTCAudioStore+AudioDeviceModuleMiddleware.o +7348
WebRTCStatsCollecting.o -6658
RTCAudioStore+AVAudioSessionEffect.o +5924
StreamCallAudioRecorder+AVAudioRecorderMiddleware.o +5695
CallAudioSession.o +5399
RTCAudioSessionPublisher.o +5319
RTCAudioStore+InterruptionEffect.o -5207
RTCAudioSessionReducer.o -4269
StereoEnableVisitor.o +4254
CallKitService.o +3927
RTCAudioStore+InterruptionsEffect.o +3808
RTCAudioStore+DefaultReducer.o +3450
RTCAudioStore+Coordinator.o +3447
RTCAudioStore+RestartAudioSession.o -3257
WebRTCCoordinator+Joining.o +3212
Call.o +2914
AudioEngineLevelNodeAdapter.o +2805
WebRTCLogger.o -2598
RTCAudioStore+AVAudioSessionConfigurationValidator.o +2478
Logger.o -2339
StoreEffect.o +1970
RTCAudioStore+WebRTCAudioSessionReducer.o +1929
PeerConnectionFactory.o +1764
RTCAudioStore+CallKitReducer.o +1702
RTCAudioDeviceModuleControlling.o +1617
Models.o +1512
RTCPeerConnectionCoordinator.o +1401
CallKitPushNotificationAdapter.o +1316
UpdateCallResponse.o +1295
RTCAudioStore+Namespace.o +1233
NoiseCancellationSettingsRequest.o +1168
CallKitAudioSessionReducer.o -1159
CallTimeline.o +1144
Encodable+Retroactive.o +1108
RTCAudioStoreAction+AudioSession.o -1078
CallController.o +989
MicrophoneManager.o +947
RTCAudioStoreAction.o -782
WebRTCStateAdapter.o +775
RTCAudioStoreAction+CallKit.o -660
Protobuf+SelectiveEncodable.o -596
StoreCoordinator.o +582
AudioSessionConfiguration.o +509
RTCAudioStore+RouteChangeEffect.o -498
Logger+ThrowingExecution.o +496
AVAudioSessionRouteDescription+Convenience.o -460
StreamDeviceOrientationAdapter.o -436
StreamCallAudioRecorder+CategoryMiddleware.o -416
PermissionsStore.o +392
MediaAdapter.o -392
LivestreamAudioSessionPolicy.o +380
StreamCallAudioRecorder+InterruptionMiddleware.o -372
CallSettings.o +371
DefaultRTCMediaConstraints.o +369
Call+Stage.o +366
ICEConnectionStateAdapter.o -336
CallStatsReportSummaryResponse.o -332
RingSettingsRequest.o -320
ConnectionState.o -312
LayoutSettings.o +296
Store.o +288
RTCAudioStoreAction+Generic.o -263
CallModerationBlurEvent.o -242
StoreLogger.o +223
BatteryStore+ObservationMiddleware.o -220
StopHLSBroadcastingResponse.o -216
WebRTCPermissionsAdapter.o -185
BatteryStore+State.o -182
StreamCallAudioRecorder+ShouldRecordMiddleware.o -176
ReflectiveStringConvertible.o -170
UserEventPayload.o +168
StoreTask.o +166
LocalAudioMediaAdapter.o +159
CallParticipant+Convenience.o +144
RTCAudioStoreMiddleware.o -138
RTCAudioStoreReducer.o -134
Reducer.o +131
CameraInterruptionsHandler.o -116
AudioProcessingStore.o +106
WebRTCCoordinator.o +92
Foundation.tbd +88
AVFAudio.tbd +88
CallReactionEvent.o +76
WebRTCCoordinator+CleanUp.o +74
IncomingVideoQualitySettings.o -72
PermissionStore+DefaultReducer.o +68
BroadcastBufferReader.o -52
StreamCallAudioRecorder+Namespace.o +49
RawJSON.o -48
Middleware.o -46
StreamRTCPeerConnection.o +44
PublisherAggregateStats.o +44
CallKitMissingPermissionPolicy+EndCall.o -44
HTTPClient.o -42

@Stream-SDK-Bot
Copy link
Collaborator

StreamVideoSwiftUI XCSize

Object Diff (bytes)
CallControlsView.o -520
ModerationWarningViewModifier.o -332
CallContainer.o +272
CallViewModel.o -175
ToastView.o +60

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants