diff --git a/src/media/audio/portaudio/audio_device_monitor.h b/src/media/audio/portaudio/audio_device_monitor.h
new file mode 100644
index 0000000000000000000000000000000000000000..9d00f5aba3412d05bfb9746e0917f568711a5753
--- /dev/null
+++ b/src/media/audio/portaudio/audio_device_monitor.h
@@ -0,0 +1,307 @@
+/*
+ *  Copyright (C) 2025 Savoir-faire Linux Inc.
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "string_utils.h"
+#include "logger.h"
+
+#include <atlbase.h>
+#include <conio.h>
+#include <mmdeviceapi.h>
+#include <mmreg.h>
+#include <string>
+#include <windows.h>
+#include <Functiondiscoverykeys_devpkey.h>
+
+#include <functional>
+#include <mutex>
+#include <unordered_set>
+
+// Note: the granularity of these events is not currently required because we end up
+// restarting the audio layer regardless of the event type. However, let's keep it like
+// this for now in case a future PortAudio bump or layer improvement can make use of it
+// to avoid unnecessary stream restarts.
+enum class DeviceEventType { BecameActive, BecameInactive, DefaultChanged, PropertyChanged };
+inline std::string
+to_string(DeviceEventType type)
+{
+    switch (type) {
+    case DeviceEventType::BecameActive:
+        return "BecameActive";
+    case DeviceEventType::BecameInactive:
+        return "BecameInactive";
+    case DeviceEventType::DefaultChanged:
+        return "DefaultChanged";
+    case DeviceEventType::PropertyChanged:
+        return "PropertyChanged";
+    }
+    return "Unknown";
+}
+using DeviceEventCallback = std::function<void(const std::string& deviceName, DeviceEventType)>;
+
+std::string
+GetFriendlyNameFromIMMDeviceId(CComPtr<IMMDeviceEnumerator> enumerator, LPCWSTR deviceId)
+{
+    if (!enumerator || !deviceId)
+        return {};
+
+    CComPtr<IMMDevice> device;
+    CComPtr<IPropertyStore> props;
+
+    if (FAILED(enumerator->GetDevice(deviceId, &device)))
+        return {};
+
+    if (FAILED(device->OpenPropertyStore(STGM_READ, &props)))
+        return {};
+
+    PROPVARIANT varName;
+    PropVariantInit(&varName);
+    if (SUCCEEDED(props->GetValue(PKEY_Device_FriendlyName, &varName))) {
+        std::wstring name = varName.pwszVal;
+        PropVariantClear(&varName);
+        return jami::to_string(name);
+    }
+
+    return {};
+}
+
+class AudioDeviceNotificationClient : public IMMNotificationClient
+{
+    LONG _refCount;
+
+public:
+    AudioDeviceNotificationClient()
+        : _refCount(1)
+    {
+        if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator),
+                                    nullptr,
+                                    CLSCTX_ALL,
+                                    __uuidof(IMMDeviceEnumerator),
+                                    (void**) &deviceEnumerator_))) {
+            JAMI_ERR("Failed to create device enumerator");
+        } else {
+            // Fill our list of active devices (render + capture)
+            enumerateDevices();
+        }
+    }
+
+    ~AudioDeviceNotificationClient()
+    {
+        if (deviceEnumerator_) {
+            deviceEnumerator_->UnregisterEndpointNotificationCallback(this);
+        }
+    }
+
+    void setDeviceEventCallback(DeviceEventCallback callback)
+    {
+        if (!deviceEnumerator_) {
+            JAMI_ERR("Device enumerator not initialized");
+            return;
+        }
+
+        {
+            std::lock_guard<std::mutex> lock(mutex_);
+            deviceEventCallback_ = callback;
+        }
+
+        // Now we can start monitoring
+        deviceEnumerator_->RegisterEndpointNotificationCallback(this);
+    }
+
+    // // IUnknown methods
+    ULONG STDMETHODCALLTYPE AddRef() override
+    {
+        auto count = InterlockedIncrement(&_refCount);
+        return count;
+    }
+
+    ULONG STDMETHODCALLTYPE Release() override
+    {
+        auto count = InterlockedDecrement(&_refCount);
+        if (count == 0) {
+            delete this;
+        }
+        return count;
+    }
+
+    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
+    {
+        if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) {
+            *ppv = static_cast<IMMNotificationClient*>(this);
+            AddRef();
+            return S_OK;
+        }
+        *ppv = nullptr;
+        return E_NOINTERFACE;
+    }
+
+    // IMMNotificationClient methods
+    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
+    {
+        handleStateChanged(deviceId, newState);
+        return S_OK;
+    }
+
+    HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
+    {
+        // Treat addition as transition to active - we'll validate the state in handler
+        handleStateChanged(deviceId, DEVICE_STATE_ACTIVE);
+        return S_OK;
+    }
+
+    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
+    {
+        // Removed devices should be treated as inactive/unavailable
+        handleStateChanged(deviceId, DEVICE_STATE_NOTPRESENT);
+        return S_OK;
+    }
+
+    HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow,
+                                                     ERole role,
+                                                     LPCWSTR deviceId) override
+    {
+        // If the default communication device changes, we should restart the layer
+        // to ensure the new device is used. We only care about the default communication
+        // device, so we ignore other roles.
+        if (role == eCommunications && deviceId && deviceEventCallback_) {
+            std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
+            deviceEventCallback_(friendlyName, DeviceEventType::DefaultChanged);
+        }
+        return S_OK;
+    }
+
+    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId,
+                                                     const PROPERTYKEY key) override
+    {
+        // Limit this to samplerate changes
+        if (key == PKEY_AudioEngine_DeviceFormat) {
+            // Fetch updated sample rate
+            IMMDevice* pDevice;
+            deviceEnumerator_->GetDevice(deviceId, &pDevice);
+
+            IPropertyStore* pProps;
+            pDevice->OpenPropertyStore(STGM_READ, &pProps);
+
+            PROPVARIANT var;
+            PropVariantInit(&var);
+            pProps->GetValue(PKEY_AudioEngine_DeviceFormat, &var);
+
+            if (var.vt == VT_BLOB) {
+                auto* pWaveFormat = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(var.blob.pBlobData);
+                UINT sampleRate = pWaveFormat->Format.nSamplesPerSec;
+                JAMI_DBG("Sample rate changed to %u", sampleRate);
+                std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_,
+                                                                          deviceId);
+                deviceEventCallback_(friendlyName, DeviceEventType::PropertyChanged);
+            }
+
+            PropVariantClear(&var);
+            pProps->Release();
+            pDevice->Release();
+        }
+        return S_OK;
+    }
+
+private:
+    CComPtr<IMMDeviceEnumerator> deviceEnumerator_;
+
+    DeviceEventCallback deviceEventCallback_;
+    std::unordered_set<std::wstring> activeDevices_;
+
+    // Notifications are invoked on system-managed threads, so we need a lock
+    // to protect deviceEventCallback_ and activeDevices_
+    std::mutex mutex_;
+
+    // Enumerates both render (playback) and capture (recording) devices
+    void enumerateDevices()
+    {
+        if (!deviceEnumerator_)
+            return;
+
+        std::lock_guard<std::mutex> lock(mutex_);
+        activeDevices_.clear();
+
+        enumerateDevicesOfType(eRender);
+        enumerateDevicesOfType(eCapture);
+    }
+
+    void enumerateDevicesOfType(EDataFlow flow)
+    {
+        CComPtr<IMMDeviceCollection> devices;
+        CComPtr<IMMDevice> device;
+        UINT deviceCount = 0;
+
+        if (FAILED(deviceEnumerator_->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &devices))) {
+            JAMI_ERR("Failed to enumerate devices");
+            return;
+        }
+
+        if (FAILED(devices->GetCount(&deviceCount))) {
+            JAMI_ERR("Failed to get device count");
+            return;
+        }
+
+        for (UINT i = 0; i < deviceCount; ++i) {
+            if (FAILED(devices->Item(i, &device)))
+                continue;
+
+            LPWSTR deviceId = nullptr;
+            if (FAILED(device->GetId(&deviceId)))
+                continue;
+
+            DWORD deviceState = 0;
+            if (SUCCEEDED(device->GetState(&deviceState)) && deviceState == DEVICE_STATE_ACTIVE) {
+                activeDevices_.insert(deviceId);
+            }
+
+            CoTaskMemFree(deviceId);
+        }
+    }
+
+    void handleStateChanged(LPCWSTR deviceId, DWORD newState)
+    {
+        if (!deviceId || !deviceEnumerator_ || !deviceEventCallback_)
+            return;
+
+        std::lock_guard<std::mutex> lock(mutex_);
+
+        std::wstring wsId(deviceId);
+        std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
+
+        // We only care if a device is entering or leaving the active state
+        bool isActive = (newState == DEVICE_STATE_ACTIVE);
+        auto it = activeDevices_.find(wsId);
+
+        if (isActive && it == activeDevices_.end()) {
+            // Device is active and not in our list? Add it and notify
+            activeDevices_.insert(wsId);
+            deviceEventCallback_(friendlyName, DeviceEventType::BecameActive);
+        } else if (!isActive && it != activeDevices_.end()) {
+            // Device is inactive and in our list? Remove it and notify
+            activeDevices_.erase(it);
+            deviceEventCallback_(friendlyName, DeviceEventType::BecameInactive);
+        }
+    }
+};
+
+// RAII COM wrapper for AudioDeviceNotificationClient
+struct AudioDeviceNotificationClient_deleter
+{
+    void operator()(AudioDeviceNotificationClient* client) const { client->Release(); }
+};
+
+typedef std::unique_ptr<AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter>
+    AudioDeviceNotificationClientPtr;
diff --git a/src/media/audio/portaudio/portaudiolayer.cpp b/src/media/audio/portaudio/portaudiolayer.cpp
index b2ad9bf925b8a6fdf006c8ffb0d34dd76bf76e57..054df62ebf6eea33828856fda3eeecc62eab99a3 100644
--- a/src/media/audio/portaudio/portaudiolayer.cpp
+++ b/src/media/audio/portaudio/portaudiolayer.cpp
@@ -16,16 +16,16 @@
  */
 
 #include "portaudiolayer.h"
-#include "manager.h"
-#include "noncopyable.h"
+
 #include "audio/resampler.h"
-#include "audio/ringbufferpool.h"
-#include "audio/ringbuffer.h"
-#include "audio/audioloop.h"
+#include "audio/portaudio/audio_device_monitor.h"
+#include "manager.h"
+#include "preferences.h"
 
 #include <portaudio.h>
+
 #include <algorithm>
-#include <cmath>
+#include <windows.h>
 
 namespace jami {
 
@@ -62,6 +62,11 @@ struct PortAudioLayer::PortAudioLayerImpl
 
     std::array<PaStream*, static_cast<int>(Direction::End)> streams_;
 
+    AudioDeviceNotificationClientPtr audioDeviceNotificationClient_;
+    // The following flag used to debounce the device state changes,
+    // as default-device change events often follow plug/unplug events.
+    std::atomic<bool> restartRequestPending_ = false;
+
     int paOutputCallback(PortAudioLayer& parent,
                          const int16_t* inputBuffer,
                          int16_t* outputBuffer,
@@ -84,7 +89,7 @@ struct PortAudioLayer::PortAudioLayerImpl
                      PaStreamCallbackFlags statusFlags);
 };
 
-//##################################################################################################
+// ##################################################################################################
 
 PortAudioLayer::PortAudioLayer(const AudioPreference& pref)
     : AudioLayer {pref}
@@ -103,6 +108,9 @@ PortAudioLayer::PortAudioLayer(const AudioPreference& pref)
         deviceInfo = Pa_GetDeviceInfo(i);
         JAMI_DBG("PortAudio device: %d, %s", i, deviceInfo->name);
     }
+
+    // Notify of device changes in case this layer was reset based on a hotplug event.
+    devicesChanged();
 }
 
 PortAudioLayer::~PortAudioLayer()
@@ -291,14 +299,34 @@ PortAudioLayer::updatePreference(AudioPreference& preference, int index, AudioDe
     }
 }
 
-//##################################################################################################
+// ##################################################################################################
 
 PortAudioLayer::PortAudioLayerImpl::PortAudioLayerImpl(PortAudioLayer& parent,
                                                        const AudioPreference& pref)
     : deviceRecord_ {pref.getPortAudioDeviceRecord()}
     , devicePlayback_ {pref.getPortAudioDevicePlayback()}
     , deviceRingtone_ {pref.getPortAudioDeviceRingtone()}
-{
+    , audioDeviceNotificationClient_ {new AudioDeviceNotificationClient}
+{
+    // Set up our callback to restart the layer on any device event
+    audioDeviceNotificationClient_->setDeviceEventCallback(
+        [this, &parent](const std::string& deviceName, const DeviceEventType event) {
+            JAMI_LOG("PortAudioLayer device event: {}, {}",
+                     deviceName.c_str(),
+                     to_string(event).c_str());
+            // Here we want to debounce the device events as a DefaultChanged could
+            // follow a DeviceAdded event and we don't want to restart the layer twice
+            if (!restartRequestPending_.exchange(true)) {
+                std::thread([] {
+                    // First wait for the debounce period to pass, allowing for multiple events
+                    // to be grouped together (e.g. DeviceAdded -> DefaultChanged).
+                    std::this_thread::sleep_for(std::chrono::milliseconds(300));
+                    auto currentAudioManager = Manager::instance().getAudioManager();
+                    Manager::instance().setAudioPlugin(currentAudioManager);
+                }).detach();
+            }
+        });
+
     init(parent);
 }
 
@@ -467,24 +495,26 @@ PaDeviceIndex
 PortAudioLayer::PortAudioLayerImpl::getApiIndexByType(AudioDeviceType type)
 {
     auto numDevices = Pa_GetDeviceCount();
-    if (numDevices < 0)
+    if (numDevices < 0) {
         JAMI_ERR("PortAudioLayer error: %s", Pa_GetErrorText(numDevices));
-    else {
+        return paNoDevice;
+    } else {
         std::string_view toMatch = (type == AudioDeviceType::CAPTURE
                                         ? deviceRecord_
                                         : (type == AudioDeviceType::PLAYBACK ? devicePlayback_
                                                                              : deviceRingtone_));
-        if (toMatch.empty())
-            return type == AudioDeviceType::CAPTURE ? Pa_GetDefaultCommInputDevice()
-                                                    : Pa_GetDefaultCommOutputDevice();
-        for (int i = 0; i < numDevices; ++i) {
-            if (const auto deviceInfo = Pa_GetDeviceInfo(i)) {
-                if (deviceInfo->name == toMatch)
-                    return i;
+        if (!toMatch.empty()) {
+            for (int i = 0; i < numDevices; ++i) {
+                if (const auto deviceInfo = Pa_GetDeviceInfo(i)) {
+                    if (deviceInfo->name == toMatch)
+                        return i;
+                }
             }
         }
     }
-    return paNoDevice;
+    // If nothing was found, return the default device
+    return type == AudioDeviceType::CAPTURE ? Pa_GetDefaultCommInputDevice()
+                                            : Pa_GetDefaultCommOutputDevice();
 }
 
 std::string