Skip to content
Open
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
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
### 🐞 Fixed
- Fix permission prompts appearing when not needed. [#988](https://github.com/GetStream/stream-video-swift/pull/988)

# [1.34.2](https://github.com/GetStream/stream-video-swift/releases/tag/1.34.2)
_October 24, 2025_
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamVideo/WebRTC/PeerConnectionFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class PeerConnectionFactory: @unchecked Sendable {
)
let decoderFactory = RTCDefaultVideoDecoderFactory()
return RTCPeerConnectionFactory(
audioDeviceModuleType: .platformDefault,
audioDeviceModuleType: .audioEngine,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we change this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platformDefault ADM causes the prompts to appear while the audioEngine based one doesn't. We are already planning on migrating to the AudioEngine ADM so we can also do it here and resolve the issue.

bypassVoiceProcessing: false,
encoderFactory: encoderFactory,
decoderFactory: decoderFactory,
Expand Down
13 changes: 11 additions & 2 deletions Sources/StreamVideo/WebRTC/v2/WebRTCPermissionsAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ final class WebRTCPermissionsAdapter: @unchecked Sendable {
private weak var delegate: WebRTCPermissionsAdapterDelegate?
private var requiredPermissions: Set<RequiredPermission> = []
private var lastCallSettings: CallSettings?
private var ownCapabilities: Set<OwnCapability> = []

/// Creates an adapter and begins observing app/permission changes.
///
Expand Down Expand Up @@ -155,6 +156,12 @@ final class WebRTCPermissionsAdapter: @unchecked Sendable {
}
}

func willSet(ownCapabilities: Set<OwnCapability>) {
processingQueue.addOperation { [weak self] in
self?.ownCapabilities = ownCapabilities
}
}

func cleanUp() {
processingQueue.addOperation { [weak self] in
// By emptying the Set we are saying that there are no permissions
Expand Down Expand Up @@ -237,9 +244,11 @@ final class WebRTCPermissionsAdapter: @unchecked Sendable {

switch permission {
case .microphone:
return !permissions.hasMicrophonePermission && permissions.canRequestMicrophonePermission
return !permissions.hasMicrophonePermission && permissions.canRequestMicrophonePermission && ownCapabilities
.contains(.sendAudio)
case .camera:
return !permissions.hasCameraPermission && permissions.canRequestCameraPermission
return !permissions.hasCameraPermission && permissions.canRequestCameraPermission && ownCapabilities
.contains(.sendVideo)
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ actor WebRTCStateAdapter: ObservableObject, StreamAudioSessionAdapterDelegate, W
func set(connectOptions value: ConnectOptions) { self.connectOptions = value }

/// Sets the own capabilities of the current user.
func set(ownCapabilities value: Set<OwnCapability>) { self.ownCapabilities = value }
func set(ownCapabilities value: Set<OwnCapability>) {
self.ownCapabilities = value
permissionsAdapter.willSet(ownCapabilities: value)
}

/// Sets the WebRTC stats reporter.
func set(statsAdapter value: WebRTCStatsAdapting?) {
Expand Down
58 changes: 54 additions & 4 deletions StreamVideoTests/WebRTC/v2/WebRTCPermissionsAdapter_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class WebRTCPermissionsAdapter_Tests: StreamVideoTestCase, @unchecked Send
super.tearDown()
}

func test_willSet_audioOnTrue_withDeniedMic_downgradesAudioOff() async {
func test_willSet_audioOnTrue_withoutSendAudio_withDeniedMic_downgradesAudioOff() async {
mockAppStateAdapter.makeShared()
mockPermissions.stubMicrophonePermission(.denied)
await fulfillment { self.mockPermissions.mockStore.state.microphonePermission == .denied }
Expand All @@ -34,7 +34,33 @@ final class WebRTCPermissionsAdapter_Tests: StreamVideoTestCase, @unchecked Send
XCTAssertEqual(output.videoOn, false)
}

func test_willSet_videoOnTrue_withDeniedCamera_downgradesVideoOff() async {
func test_willSet_audioOnTrue_withSendAudio_withDeniedMic_downgradesAudioOff() async {
mockAppStateAdapter.makeShared()
mockPermissions.stubMicrophonePermission(.denied)
await fulfillment { self.mockPermissions.mockStore.state.microphonePermission == .denied }

let input = CallSettings(audioOn: true, videoOn: false)
subject.willSet(ownCapabilities: [.sendAudio])
let output = await subject.willSet(callSettings: input)

XCTAssertEqual(output.audioOn, false)
XCTAssertEqual(output.videoOn, false)
}

func test_willSet_videoOnTrue_withSendVideo_withDeniedCamera_downgradesVideoOff() async {
mockAppStateAdapter.makeShared()
mockPermissions.stubCameraPermission(.denied)
await fulfillment { self.mockPermissions.mockStore.state.cameraPermission == .denied }

let input = CallSettings(audioOn: false, videoOn: true)
subject.willSet(ownCapabilities: [.sendVideo])
let output = await subject.willSet(callSettings: input)

XCTAssertEqual(output.audioOn, false)
XCTAssertEqual(output.videoOn, false)
}

func test_willSet_videoOnTrue_withoutSendVideo_withDeniedCamera_downgradesVideoOff() async {
mockAppStateAdapter.makeShared()
mockPermissions.stubCameraPermission(.denied)
await fulfillment { self.mockPermissions.mockStore.state.cameraPermission == .denied }
Expand All @@ -46,10 +72,22 @@ final class WebRTCPermissionsAdapter_Tests: StreamVideoTestCase, @unchecked Send
XCTAssertEqual(output.videoOn, false)
}

func test_willSet_audioOnTrue_unknownMic_inForeground_requestsPermission_andKeepsAudioOnWhenGranted() async {
func test_willSet_audioOnTrue_unknownMic_inForeground_withoutSendAudio_requestsPermission_andKeepsAudioOnWhenGranted() async {
mockAppStateAdapter.makeShared()
mockAppStateAdapter.stubbedState = .foreground
mockPermissions.stubMicrophonePermission(.unknown)
await fulfillment { self.mockPermissions.mockStore.state.microphonePermission == .unknown }

let input = CallSettings(audioOn: true, videoOn: false)
let output = await self.subject.willSet(callSettings: input)
XCTAssertEqual(output.audioOn, false)
}

func test_willSet_audioOnTrue_unknownMic_inForeground_withSendAudio_requestsPermission_andKeepsAudioOnWhenGranted() async {
mockAppStateAdapter.makeShared()
mockAppStateAdapter.stubbedState = .foreground
mockPermissions.stubMicrophonePermission(.unknown)
subject.willSet(ownCapabilities: [.sendAudio])
await fulfillment { self.mockPermissions.mockStore.state.microphonePermission == .unknown }

await withTaskGroup(of: Void.self) { group in
Expand All @@ -71,10 +109,22 @@ final class WebRTCPermissionsAdapter_Tests: StreamVideoTestCase, @unchecked Send
}
}

func test_willSet_videoOnTrue_unknownCamera_inForeground_requestsPermission_andKeepsVideoOnWhenGranted() async {
func test_willSet_videoOnTrue_unknownCamera_inForeground_withoutSendVideo_requestsPermission_andKeepsVideoOnWhenGranted() async {
mockAppStateAdapter.makeShared()
mockAppStateAdapter.stubbedState = .foreground
mockPermissions.stubCameraPermission(.unknown)
await fulfillment { self.mockPermissions.mockStore.state.cameraPermission == .unknown }

let input = CallSettings(audioOn: false, videoOn: true)
let output = await self.subject.willSet(callSettings: input)
XCTAssertEqual(output.videoOn, false)
}

func test_willSet_videoOnTrue_unknownCamera_inForeground_withSendVideo_requestsPermission_andKeepsVideoOnWhenGranted() async {
mockAppStateAdapter.makeShared()
mockAppStateAdapter.stubbedState = .foreground
mockPermissions.stubCameraPermission(.unknown)
subject.willSet(ownCapabilities: [.sendVideo])
await fulfillment { self.mockPermissions.mockStore.state.cameraPermission == .unknown }

await withTaskGroup(of: Void.self) { group in
Expand Down