Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8afd797
Add IOS native plugin to configure audio session + enable native audi…
sierpinskid Apr 22, 2026
f948166
update the native plugin
sierpinskid Apr 23, 2026
d8a5a85
Fix using the native audio integration + fix calling the android spec…
sierpinskid Apr 23, 2026
c4ff580
Improve configuring audio session for the webrc session
sierpinskid Apr 24, 2026
4d02a5d
update the native plugin
sierpinskid Apr 24, 2026
61029a3
ensure the IOS audio session is properly configured for webrtc session
sierpinskid Apr 24, 2026
24a9154
Add IOS audio session cleanup after stoping the call
sierpinskid Apr 28, 2026
29c00fd
add IOS audio session method swizzling to log audio session change ca…
sierpinskid Apr 30, 2026
880018b
Add debug log + prevent null ref if pins are empty
sierpinskid Apr 30, 2026
22e99a6
Add null checks to easier track down the null ref
sierpinskid Apr 30, 2026
dc5fa2d
clear session after a failed join
sierpinskid May 4, 2026
f42cfdd
add debug logging
sierpinskid May 4, 2026
2379f68
refactor debug logs
sierpinskid May 4, 2026
19b3eba
update the native plugin
sierpinskid May 4, 2026
9455ab1
remove debug/diagnostic code
sierpinskid May 8, 2026
67da3a9
simplify comments + rename method
sierpinskid May 11, 2026
d33f80b
update the native plugin
sierpinskid May 11, 2026
e7faa37
Expose CallLeaving event
sierpinskid Jun 22, 2026
2fe094c
Update io.stream.unity.webrtc from uni-170 AEC fork
sierpinskid Jun 24, 2026
09a393a
Add iOS webrtc.framework.dSYM from uni-170 AEC fork
sierpinskid Jun 24, 2026
9a5e4a4
fix compiler error
sierpinskid Jun 24, 2026
83a2110
update native binary
sierpinskid Jun 24, 2026
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/iOS/webrtc.framework.dSYM/Contents/Resources/DWARF/webrtc filter=lfs diff=lfs merge=lfs -text
Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/iOS/webrtc.framework.dSYM/Contents/Resources/Relocations/**/*.yml filter=lfs diff=lfs merge=lfs -text
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ protected void OnApplicationPause(bool pauseStatus)
if (pauseStatus)
{
// App is going to background
Client.PauseAndroidAudioPlayback();
Client.PauseMobileAudioPlayback();
_wasAudioPublishEnabledOnPause = Client.AudioDeviceManager.IsEnabled;
_wasVideoPublishEnabledOnPause = Client.VideoDeviceManager.IsEnabled;

Expand All @@ -226,7 +226,7 @@ protected void OnApplicationPause(bool pauseStatus)
else
{
// App is coming to foreground
Client.ResumeAndroidAudioPlayback();
Client.ResumeMobileAudioPlayback();

if (_wasAudioPublishEnabledOnPause)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public override IEnumerable<MicrophoneDeviceInfo> EnumerateDevices()
{
}

#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
if (RtcSession.UseNativeAudioBindings)
{
NativeAudioDeviceManager.GetAudioInputDevices(ref _inputDevicesBuffer);
Expand All @@ -43,13 +44,13 @@ public override IEnumerable<MicrophoneDeviceInfo> EnumerateDevices()

yield return new MicrophoneDeviceInfo(device.Id, device.Name);
}
yield break;
}
else
#endif

foreach (var deviceName in Microphone.devices)
{
foreach (var deviceName in Microphone.devices)
{
yield return new MicrophoneDeviceInfo(deviceName);
}
yield return new MicrophoneDeviceInfo(deviceName);
}
}

Expand Down Expand Up @@ -129,10 +130,12 @@ public void SelectDevice(MicrophoneDeviceInfo device, bool enable)

IsEnabled = enable;

#if UNITY_ANDROID && !UNITY_EDITOR
if (RtcSession.UseNativeAudioBindings)
{
SetAudioRoutingAsync((NativeAudioDeviceManager.AudioRouting)SelectedDevice.IntId.Value).LogIfFailed();
}
#endif
}

//StreamTodo: https://docs.unity3d.com/ScriptReference/AudioSource-ignoreListenerPause.html perhaps this should be enabled so that AudioListener doesn't affect recorded audio
Expand Down
18 changes: 12 additions & 6 deletions Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public interface IStreamVideoClient : IStreamVideoClientEventsListener, IDisposa
/// </summary>
event CallHandler CallStarted;

/// <summary>
/// Event fired when a call is about to be left. You can still access full call data because the leaving process is just starting.
/// </summary>
event CallHandler CallLeaving;

/// <summary>
/// Event fired when a call ended
/// </summary>
Expand Down Expand Up @@ -129,16 +134,17 @@ Task<IStreamCall> GetCallAsync(StreamCallType callType, string callId,


/// <summary>
/// Temporary method (can be removed in the future) to pause audio playback on Android.
/// This will completely suspend playback of any audio coming from the StreamVideo SDK on the Android platform.
/// Mutes native SDK audio playback (silences remote participants on the device's
/// speakers). Call from app lifecycle events such as <c>OnApplicationPause(true)</c>.
/// The native audio device stays open so resume is instant. No-op on platforms
/// that don't use the native audio pipeline (Editor, Standalone).
/// </summary>
void PauseAndroidAudioPlayback();
void PauseMobileAudioPlayback();

/// <summary>
/// Temporary method (can be removed in the future) to resume audio playback on Android.
/// Call this resume audio playback if it was previously paused using <see cref="PauseAndroidAudioPlayback"/>.
/// Resumes playback previously muted via <see cref="PauseMobileAudioPlayback"/>.
/// </summary>
void ResumeAndroidAudioPlayback();
void ResumeMobileAudioPlayback();

/// <summary>
/// Set Android Audio usage mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//StreamTodo: duplicated declaration of STREAM_NATIVE_AUDIO (also in RtcSession.cs) easy to get out of sync.

#if UNITY_ANDROID && !UNITY_EDITOR
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
#define STREAM_NATIVE_AUDIO //Defined in multiple files
#endif
using System;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if UNITY_ANDROID && !UNITY_EDITOR
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
#define STREAM_NATIVE_AUDIO //Defined in multiple files
#endif
using System;
Expand Down
110 changes: 87 additions & 23 deletions Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if UNITY_ANDROID && !UNITY_EDITOR
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
#define STREAM_NATIVE_AUDIO //Defined in multiple files
#endif
using System;
Expand Down Expand Up @@ -67,7 +67,6 @@ internal class RtcSession : IMediaInputProvider, ISfuClient, IDisposable

// Some sources claim the 48kHz is the most optimal sample rate for WebRTC, other cause internal resampling
public const int AudioOutputSampleRate = 48_000;
public const int AudioOutputChannels = 2;

#if STREAM_NATIVE_AUDIO
public const bool UseNativeAudioBindings = true;
Expand Down Expand Up @@ -588,7 +587,16 @@ public async Task DoJoin(JoinCallData joinCallData, CancellationToken cancellati

//StreamTODO: if we try to rejoin a call with no other participants we'll get error from SFU not call FOUND
// What should we do then?


if (ActiveCall == null)
{
throw new Exception("ActiveCall should never be null here.");
}

if (joinResponse == null)
{
throw new Exception("joinResponse was null");
}

ActiveCall.UpdateFromSfu(joinResponse);
_logs.WarningIfDebug($"{nameof(DoJoin)} - SFU Sending join response received. startNewPeerConnections: {startNewPeerConnections}");
Expand Down Expand Up @@ -660,7 +668,10 @@ public async Task DoJoin(JoinCallData joinCallData, CancellationToken cancellati
{
//StreamTODO: Either use UseNativeAudioBindings const or STREAM_NATIVE_AUDIO flag but not both. Once we replace the webRTC package we could remove STREAM_NATIVE_AUDIO
#if STREAM_NATIVE_AUDIO
WebRTC.StartAudioPlayback(AudioOutputSampleRate, AudioOutputChannels);
// iOS VPIO requires PlayAndRecord before the audio unit opens (no automatic retry on '!rec').
EnsureIOSAudioSessionReadyForVPIO($"{nameof(DoJoin)} StartAudioPlayback");

WebRTC.StartAudioPlayback(AudioOutputSampleRate);
#endif
}

Expand All @@ -675,6 +686,15 @@ public async Task DoJoin(JoinCallData joinCallData, CancellationToken cancellati
{
CallState = prevCallState;
}

try
{
await ClearSessionAsync();
}
catch (Exception cleanupEx)
{
_logs.Warning($"{nameof(DoJoin)} session cleanup after failure encountered an error: {cleanupEx.Message}");
}

throw;
}
Expand Down Expand Up @@ -731,7 +751,18 @@ public Task StopAsync(string reason = "")
if (UseNativeAudioBindings)
{
#if STREAM_NATIVE_AUDIO
// iOS order: stop playback + capture (closes the duplex VPIO unit),
// THEN deactivate AVAudioSession. Reverse order returns IsBusy.
WebRTC.StopAudioPlayback();

#if UNITY_IOS && !UNITY_EDITOR
if (Publisher?.PublisherAudioTrack != null)
{
Publisher.PublisherAudioTrack.StopLocalAudioCapture();
}

Libs.iOSAudioManagers.IOSAudioManager.DeconfigureAudioSession();
#endif
#endif
}

Expand Down Expand Up @@ -882,37 +913,30 @@ public void SetAudioRecordingDevice(MicrophoneDeviceInfo device)

public void TryRestartAudioPlayback()
{
if (!UseNativeAudioBindings)
if (UseNativeAudioBindings)
{
return;
}
#if STREAM_NATIVE_AUDIO
WebRTC.StopAudioPlayback();
WebRTC.StartAudioPlayback(AudioOutputSampleRate, AudioOutputChannels);
WebRTC.StopAudioPlayback();
EnsureIOSAudioSessionReadyForVPIO(nameof(TryRestartAudioPlayback));
WebRTC.StartAudioPlayback(AudioOutputSampleRate);
#endif
}
}

//StreamTODO: temp solution to allow stopping the audio when app is minimized. User tried disabling the AudioSource but the audio is handled natively so it has no effect
public void PauseAndroidAudioPlayback()
// Mutes the AudioMixer so the playback callback writes silence to the speakers.
// The native audio device keeps running; on iOS this also keeps the VPIO audio
// unit alive so unmuting is instant and does not need a session reconfigure.
public void PauseMobileAudioPlayback()
{
#if STREAM_NATIVE_AUDIO
WebRTC.MuteAndroidAudioPlayback();
_logs.Warning("Audio Playback is paused. This stops all audio coming from StreamVideo SDK on Android platform.");
#else
throw new NotSupportedException(
$"{nameof(PauseAndroidAudioPlayback)} is only supported on Android platform.");
WebRTC.MuteAudioPlayback();
#endif
}

//StreamTODO: temp solution to allow stopping the audio when app is minimized. User tried disabling the AudioSource but the audio is handled natively so it has no effect
public void ResumeAndroidAudioPlayback()
public void ResumeMobileAudioPlayback()
{
#if STREAM_NATIVE_AUDIO
WebRTC.UnmuteAndroidAudioPlayback();
_logs.Warning("Audio Playback is resumed. This resumes audio coming from StreamVideo SDK on Android platform.");
#else
throw new NotSupportedException(
$"{nameof(ResumeAndroidAudioPlayback)} is only supported on Android platform.");
WebRTC.UnmuteAudioPlayback();
#endif
}

Expand Down Expand Up @@ -1416,13 +1440,53 @@ private void UpdateAudioRecording()
//StreamTODO: implement proper passing deviceID -> for Android and IOS we're skipping the deviceID
//because they operate on audio routing instead of actual devices. The underlying native implementation for Android let's OS pick the preferred device

// Configure session BEFORE capture so VPIO opens with HW AEC/NS/AGC from sample 0.
EnsureIOSAudioSessionReadyForVPIO($"{nameof(UpdateAudioRecording)} StartLocalAudioCapture");

_logs.WarningIfDebug("RtcSession.UpdateAudioRecording -> START local audio capture");
Publisher.PublisherAudioTrack.StartLocalAudioCapture(-1, AudioInputSampleRate);

#if UNITY_IOS && !UNITY_EDITOR
if (!Libs.iOSAudioManagers.IOSAudioManager.IsHardwareNoiseCancellationActive)
{
_logs.Warning(
"RtcSession.UpdateAudioRecording -> iOS hardware noise cancellation (VoiceProcessingIO) NOT active. "
+ "AEC/NS/AGC will not be applied. Check that no other plugin overrode AVAudioSession mode.");
}
#endif
}
else
{
_logs.WarningIfDebug("RtcSession.UpdateAudioRecording -> STOP local audio capture");
Publisher.PublisherAudioTrack.StopLocalAudioCapture();

// Do NOT deactivate the AVAudioSession on mute: the duplex VPIO unit is
// shared with playback and would also stop, killing remote audio. The
// session is deactivated in StopAsync after both directions are torn down.
}
#endif
}

/// <summary>
/// Configure the iOS AVAudioSession for VPIO (PlayAndRecord + VideoChat) before
/// opening the duplex audio unit. Verifies the result and retries once to catch
/// races with other components reasserting the session. No-op on non-iOS targets.
/// </summary>
private void EnsureIOSAudioSessionReadyForVPIO(string callerContext)
{
#if UNITY_IOS && !UNITY_EDITOR
var configureOk = Libs.iOSAudioManagers.IOSAudioManager.ConfigureForWebRTC();
if (configureOk && Libs.iOSAudioManagers.IOSAudioManager.IsHardwareNoiseCancellationActive)
{
return;
}

configureOk = Libs.iOSAudioManagers.IOSAudioManager.ConfigureForWebRTC();
if (!configureOk || !Libs.iOSAudioManagers.IOSAudioManager.IsHardwareNoiseCancellationActive)
{
_logs.Error(
$"RtcSession.{nameof(EnsureIOSAudioSessionReadyForVPIO)} ({callerContext}): session not VPIO-ready after retry. "
+ "Next StartAudio call may fail with '!rec'. See [StreamVideo iOS Audio] device log for NSError details.");
}
#endif
}
Expand Down
Loading
Loading