diff --git a/Runtime/Scripts/AudioStream.cs b/Runtime/Scripts/AudioStream.cs index 7915f466..834cc534 100644 --- a/Runtime/Scripts/AudioStream.cs +++ b/Runtime/Scripts/AudioStream.cs @@ -15,7 +15,7 @@ public sealed class AudioStream : IDisposable { internal readonly FfiHandle Handle; private readonly AudioSource _audioSource; - private readonly AudioProbe _probe; + private AudioProbe _probe; private RingBuffer _buffer; private short[] _tempBuffer; private short[] _crossfadeScratch; @@ -73,6 +73,11 @@ public AudioStream(RemoteAudioTrack audioTrack, AudioSource source) // Subscribe to application pause events to handle background/foreground transitions MonoBehaviourContext.OnApplicationPauseEvent += OnApplicationPause; + + // Unity stops every AudioSource when the system audio output device changes + // (e.g. headphones unplugged). Without re-playing the source, OnAudioFilterRead + // stops firing and the stream goes silent until the AudioStream is recreated. + AudioSettings.OnAudioConfigurationChanged += OnAudioConfigurationChanged; } // Called on Unity audio thread @@ -211,6 +216,34 @@ static float S16ToFloat(short v) } } + // Called when the system audio output device changes (e.g. plug/unplug headphones) + // or AudioSettings.Reset is invoked. Unity tears down its audio engine in both cases, + // which stops every AudioSource and detaches the AudioProbe filter from the rebuilt + // audio graph. Just calling Play() on the existing source isn't always enough; we + // additionally recreate the AudioProbe so Unity re-registers the OnAudioFilterRead + // node on the new graph. + private void OnAudioConfigurationChanged(bool deviceWasChanged) + { + if (_disposed) return; + + lock (_lock) + { + _buffer?.Clear(); + _isPrimed = false; + } + + if (_probe != null) + { + _probe.AudioRead -= OnAudioRead; + UnityEngine.Object.Destroy(_probe); + } + _probe = _audioSource.gameObject.AddComponent(); + _probe.AudioRead += OnAudioRead; + + _audioSource.Stop(); + _audioSource.Play(); + } + // Called when application goes to background or returns to foreground internal void OnApplicationPause(bool pause) { @@ -285,6 +318,7 @@ private void Dispose(bool disposing) // touching partially disposed state. FfiClient.Instance.AudioStreamEventReceived -= OnAudioStreamEvent; MonoBehaviourContext.OnApplicationPauseEvent -= OnApplicationPause; + AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigurationChanged; lock (_lock) { diff --git a/Tests/EditMode/MediaStreamLifetimeTests.cs b/Tests/EditMode/MediaStreamLifetimeTests.cs index 425212d9..0209bfa2 100644 --- a/Tests/EditMode/MediaStreamLifetimeTests.cs +++ b/Tests/EditMode/MediaStreamLifetimeTests.cs @@ -122,6 +122,7 @@ public void AudioStream_Dispose_UnsubscribesAndReleasesOwnedResources() StringAssert.Contains("FfiClient.Instance.AudioStreamEventReceived -= OnAudioStreamEvent;", source); StringAssert.Contains("_probe.AudioRead -= OnAudioRead;", source); + StringAssert.Contains("AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigurationChanged;", source); StringAssert.Contains("_buffer?.Dispose();", source); StringAssert.Contains("_resampler?.Dispose();", source); StringAssert.Contains("Handle.Dispose();", source);