Module livekit.rtc.platform_audio
PlatformAudio - Platform audio device management via WebRTC's ADM
PlatformAudio provides access to the platform's audio devices (microphones and speakers) via WebRTC's Audio Device Module (ADM). This is the recommended way to handle audio in most applications because:
- Built-in echo cancellation (AEC), noise suppression (NS), and auto gain control (AGC)
- Automatic device enumeration and selection
- Efficient hardware-accelerated audio processing on supported platforms
- No need for external audio libraries
For custom audio processing pipelines where you need direct access to audio frames, use the synthetic mode with AudioSource instead. See AudioSource for more details.
Usage
from livekit import rtc
# Create PlatformAudio instance (enables ADM)
platform_audio = rtc.PlatformAudio()
# List available devices
print("Microphones:")
for device in platform_audio.recording_devices():
print(f" [{device.index}] {device.name} (ID: {device.id})")
print("Speakers:")
for device in platform_audio.playout_devices():
print(f" [{device.index}] {device.name} (ID: {device.id})")
# Select specific devices (optional - uses default if not called)
platform_audio.set_recording_device(mic_device.id)
platform_audio.set_playout_device(speaker_device.id)
# Create audio source for publishing
audio_source = platform_audio.create_audio_source()
# Create and publish track
track = rtc.LocalAudioTrack.create_audio_track("microphone", audio_source)
await room.local_participant.publish_track(track)
Limitations
- No direct access to captured audio frames (ADM sends directly to WebRTC)
- For frame-level processing, use synthetic mode with AudioSource.capture_frame()
Classes
class AudioDeviceInfo (index: int, name: str, id: str)-
Expand source code
@dataclass class AudioDeviceInfo: """Information about an audio device. Attributes: index: Device index (0-based). Note: indices can change when devices are added/removed, so prefer using `id` for device selection. name: Device name as reported by the operating system. id: Platform-specific unique device identifier (GUID). This is stable across device additions/removals and should be preferred over index for device selection. """ index: int name: str id: str @classmethod def _from_proto(cls, proto: proto_audio_frame.AudioDeviceInfo) -> "AudioDeviceInfo": return cls(index=proto.index, name=proto.name, id=proto.guid)Information about an audio device.
Attributes
index- Device index (0-based). Note: indices can change when devices are
added/removed, so prefer using
idfor device selection. name- Device name as reported by the operating system.
id- Platform-specific unique device identifier (GUID). This is stable across device additions/removals and should be preferred over index for device selection.
Instance variables
var id : strvar index : intvar name : str
class PlatformAudio-
Expand source code
class PlatformAudio: """Platform audio device management via WebRTC's Audio Device Module (ADM). PlatformAudio provides the recommended way to handle audio capture and playback in LiveKit applications. It uses WebRTC's ADM for efficient, hardware-accelerated audio processing with built-in echo cancellation, noise suppression, and automatic gain control. Key features: - Device enumeration: List available microphones and speakers - Device selection: Choose specific input/output devices - Audio processing: Built-in AEC, NS, and AGC - Automatic playout: Received audio is automatically played through speakers Example: ```python # Create PlatformAudio (keeps ADM alive) platform_audio = rtc.PlatformAudio() # Enumerate and select devices mics = platform_audio.recording_devices() platform_audio.set_recording_device(mics[0].id) # Create source and publish track source = platform_audio.create_audio_source() track = rtc.LocalAudioTrack.create_audio_track("mic", source) await room.local_participant.publish_track(track) # When done, close resources source.close() platform_audio.close() ``` Resource Management: Call `close()` when done to immediately release native ADM resources. If not called, resources are released via garbage collection, but GC timing is non-deterministic. On Windows especially, failing to promptly release ADM resources can cause audio device contention and interfere with subsequent audio operations (including synthetic AudioSource usage). """ def __init__(self) -> None: """Initialize PlatformAudio and enable the Audio Device Module. Raises: PlatformAudioError: If ADM initialization fails. """ req = proto_ffi.FfiRequest() req.new_platform_audio.SetInParent() resp = FfiClient.instance.request(req) if resp.new_platform_audio.HasField("error"): raise PlatformAudioError( f"Failed to initialize PlatformAudio: {resp.new_platform_audio.error}" ) self._info = resp.new_platform_audio.platform_audio.info self._ffi_handle = FfiHandle(resp.new_platform_audio.platform_audio.handle.id) def recording_devices(self) -> List[AudioDeviceInfo]: """Get available recording devices (microphones). Returns: List of AudioDeviceInfo for each available microphone. Raises: PlatformAudioError: If device enumeration fails. """ req = proto_ffi.FfiRequest() req.get_audio_devices.platform_audio_handle = self._ffi_handle.handle resp = FfiClient.instance.request(req) if resp.get_audio_devices.HasField("error") and resp.get_audio_devices.error: raise PlatformAudioError( f"Failed to enumerate recording devices: {resp.get_audio_devices.error}" ) return [AudioDeviceInfo._from_proto(d) for d in resp.get_audio_devices.recording_devices] def playout_devices(self) -> List[AudioDeviceInfo]: """Get available playout devices (speakers/headphones). Returns: List of AudioDeviceInfo for each available speaker. Raises: PlatformAudioError: If device enumeration fails. """ req = proto_ffi.FfiRequest() req.get_audio_devices.platform_audio_handle = self._ffi_handle.handle resp = FfiClient.instance.request(req) if resp.get_audio_devices.HasField("error") and resp.get_audio_devices.error: raise PlatformAudioError( f"Failed to enumerate playout devices: {resp.get_audio_devices.error}" ) return [AudioDeviceInfo._from_proto(d) for d in resp.get_audio_devices.playout_devices] def set_recording_device(self, device_id: str) -> None: """Select the recording device (microphone) to use. Call this before creating audio tracks to select which microphone to use. If not called, the system default microphone is used. Args: device_id: The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events. Raises: PlatformAudioError: If device selection fails. """ req = proto_ffi.FfiRequest() req.set_recording_device.platform_audio_handle = self._ffi_handle.handle req.set_recording_device.device_id = device_id resp = FfiClient.instance.request(req) if resp.set_recording_device.HasField("error") and resp.set_recording_device.error: raise PlatformAudioError( f"Failed to set recording device: {resp.set_recording_device.error}" ) def set_playout_device(self, device_id: str) -> None: """Select the playout device (speaker) to use. Call this before connecting to a room to select which speaker to use for audio output. If not called, the system default speaker is used. Args: device_id: The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events. Raises: PlatformAudioError: If device selection fails. """ req = proto_ffi.FfiRequest() req.set_playout_device.platform_audio_handle = self._ffi_handle.handle req.set_playout_device.device_id = device_id resp = FfiClient.instance.request(req) if resp.set_playout_device.HasField("error") and resp.set_playout_device.error: raise PlatformAudioError( f"Failed to set playout device: {resp.set_playout_device.error}" ) def create_audio_source( self, options: Optional[PlatformAudioOptions] = None ) -> PlatformAudioSource: """Create an audio source for publishing microphone audio. The returned source captures audio from the selected microphone (or system default) and can be used with LocalAudioTrack.create_audio_track() to publish audio. Args: options: Audio processing options (AEC, NS, AGC, prefer_hardware). If None, defaults are used (all processing enabled). Returns: PlatformAudioSource that can be passed to LocalAudioTrack.create_audio_track(). Raises: PlatformAudioError: If source creation fails. Example: ```python source = platform_audio.create_audio_source( PlatformAudioOptions( echo_cancellation=True, noise_suppression=True, auto_gain_control=True, ) ) track = rtc.LocalAudioTrack.create_audio_track("microphone", source) ``` """ if options is None: options = PlatformAudioOptions() req = proto_ffi.FfiRequest() req.new_audio_source.type = proto_audio_frame.AudioSourceType.AUDIO_SOURCE_PLATFORM req.new_audio_source.platform_audio_handle = self._ffi_handle.handle req.new_audio_source.options.CopyFrom(options._to_proto()) # For platform audio, the ADM determines the actual sample rate and channels. # These fields are ignored but required by older proto versions (will be optional # in livekit-ffi >= 0.12.58). Use standard WebRTC defaults. req.new_audio_source.sample_rate = 48000 req.new_audio_source.num_channels = 1 resp = FfiClient.instance.request(req) source_info = resp.new_audio_source.source return PlatformAudioSource( FfiHandle(source_info.handle.id), source_info, ) def close(self) -> None: """Release the native Audio Device Module resources. Call this method when you are done using PlatformAudio to immediately release the underlying ADM handle and associated native resources. Why call close() explicitly? Python's garbage collection is non-deterministic. If you rely on GC to clean up PlatformAudio, the ADM may remain active longer than expected. This can cause problems on some platforms (especially Windows) where: - Audio device handles are held longer than necessary, preventing other applications or audio subsystems from accessing the devices - Subsequent audio operations (including synthetic AudioSource) may fail or behave unexpectedly due to ADM still being active - Tests running in sequence may interfere with each other if the previous test's ADM resources haven't been collected yet What happens if close() is not called? Resources will eventually be released when the PlatformAudio object is garbage collected. This works fine in simple applications where the object goes out of scope and is collected promptly. However, in these scenarios you should call close() explicitly: - Running multiple tests that use audio - Switching between PlatformAudio and synthetic AudioSource - Long-running applications that create/destroy audio resources - Applications where prompt device release is important It is safe to call `close()` multiple times; subsequent calls are no-ops. Note: Always close PlatformAudioSource instances before closing the parent PlatformAudio instance. """ self._ffi_handle.dispose()Platform audio device management via WebRTC's Audio Device Module (ADM).
PlatformAudio provides the recommended way to handle audio capture and playback in LiveKit applications. It uses WebRTC's ADM for efficient, hardware-accelerated audio processing with built-in echo cancellation, noise suppression, and automatic gain control.
Key features: - Device enumeration: List available microphones and speakers - Device selection: Choose specific input/output devices - Audio processing: Built-in AEC, NS, and AGC - Automatic playout: Received audio is automatically played through speakers
Example
# Create PlatformAudio (keeps ADM alive) platform_audio = rtc.PlatformAudio() # Enumerate and select devices mics = platform_audio.recording_devices() platform_audio.set_recording_device(mics[0].id) # Create source and publish track source = platform_audio.create_audio_source() track = rtc.LocalAudioTrack.create_audio_track("mic", source) await room.local_participant.publish_track(track) # When done, close resources source.close() platform_audio.close()Resource Management: Call
close()when done to immediately release native ADM resources. If not called, resources are released via garbage collection, but GC timing is non-deterministic. On Windows especially, failing to promptly release ADM resources can cause audio device contention and interfere with subsequent audio operations (including synthetic AudioSource usage).Initialize PlatformAudio and enable the Audio Device Module.
Raises
PlatformAudioError- If ADM initialization fails.
Methods
def close(self) ‑> None-
Expand source code
def close(self) -> None: """Release the native Audio Device Module resources. Call this method when you are done using PlatformAudio to immediately release the underlying ADM handle and associated native resources. Why call close() explicitly? Python's garbage collection is non-deterministic. If you rely on GC to clean up PlatformAudio, the ADM may remain active longer than expected. This can cause problems on some platforms (especially Windows) where: - Audio device handles are held longer than necessary, preventing other applications or audio subsystems from accessing the devices - Subsequent audio operations (including synthetic AudioSource) may fail or behave unexpectedly due to ADM still being active - Tests running in sequence may interfere with each other if the previous test's ADM resources haven't been collected yet What happens if close() is not called? Resources will eventually be released when the PlatformAudio object is garbage collected. This works fine in simple applications where the object goes out of scope and is collected promptly. However, in these scenarios you should call close() explicitly: - Running multiple tests that use audio - Switching between PlatformAudio and synthetic AudioSource - Long-running applications that create/destroy audio resources - Applications where prompt device release is important It is safe to call `close()` multiple times; subsequent calls are no-ops. Note: Always close PlatformAudioSource instances before closing the parent PlatformAudio instance. """ self._ffi_handle.dispose()Release the native Audio Device Module resources.
Call this method when you are done using PlatformAudio to immediately release the underlying ADM handle and associated native resources.
Why call close() explicitly? Python's garbage collection is non-deterministic. If you rely on GC to clean up PlatformAudio, the ADM may remain active longer than expected. This can cause problems on some platforms (especially Windows) where:
- Audio device handles are held longer than necessary, preventing other applications or audio subsystems from accessing the devices - Subsequent audio operations (including synthetic AudioSource) may fail or behave unexpectedly due to ADM still being active - Tests running in sequence may interfere with each other if the previous test's ADM resources haven't been collected yetWhat happens if close() is not called? Resources will eventually be released when the PlatformAudio object is garbage collected. This works fine in simple applications where the object goes out of scope and is collected promptly. However, in these scenarios you should call close() explicitly:
- Running multiple tests that use audio - Switching between PlatformAudio and synthetic AudioSource - Long-running applications that create/destroy audio resources - Applications where prompt device release is importantIt is safe to call
close()multiple times; subsequent calls are no-ops.Note
Always close PlatformAudioSource instances before closing the parent PlatformAudio instance.
def create_audio_source(self,
options: Optional[PlatformAudioOptions] = None) ‑> PlatformAudioSource-
Expand source code
def create_audio_source( self, options: Optional[PlatformAudioOptions] = None ) -> PlatformAudioSource: """Create an audio source for publishing microphone audio. The returned source captures audio from the selected microphone (or system default) and can be used with LocalAudioTrack.create_audio_track() to publish audio. Args: options: Audio processing options (AEC, NS, AGC, prefer_hardware). If None, defaults are used (all processing enabled). Returns: PlatformAudioSource that can be passed to LocalAudioTrack.create_audio_track(). Raises: PlatformAudioError: If source creation fails. Example: ```python source = platform_audio.create_audio_source( PlatformAudioOptions( echo_cancellation=True, noise_suppression=True, auto_gain_control=True, ) ) track = rtc.LocalAudioTrack.create_audio_track("microphone", source) ``` """ if options is None: options = PlatformAudioOptions() req = proto_ffi.FfiRequest() req.new_audio_source.type = proto_audio_frame.AudioSourceType.AUDIO_SOURCE_PLATFORM req.new_audio_source.platform_audio_handle = self._ffi_handle.handle req.new_audio_source.options.CopyFrom(options._to_proto()) # For platform audio, the ADM determines the actual sample rate and channels. # These fields are ignored but required by older proto versions (will be optional # in livekit-ffi >= 0.12.58). Use standard WebRTC defaults. req.new_audio_source.sample_rate = 48000 req.new_audio_source.num_channels = 1 resp = FfiClient.instance.request(req) source_info = resp.new_audio_source.source return PlatformAudioSource( FfiHandle(source_info.handle.id), source_info, )Create an audio source for publishing microphone audio.
The returned source captures audio from the selected microphone (or system default) and can be used with LocalAudioTrack.create_audio_track() to publish audio.
Args
options- Audio processing options (AEC, NS, AGC, prefer_hardware). If None, defaults are used (all processing enabled).
Returns
PlatformAudioSource that can be passed to LocalAudioTrack.create_audio_track().
Raises
PlatformAudioError- If source creation fails.
Example
source = platform_audio.create_audio_source( PlatformAudioOptions( echo_cancellation=True, noise_suppression=True, auto_gain_control=True, ) ) track = rtc.LocalAudioTrack.create_audio_track("microphone", source) def playout_devices(self) ‑> List[AudioDeviceInfo]-
Expand source code
def playout_devices(self) -> List[AudioDeviceInfo]: """Get available playout devices (speakers/headphones). Returns: List of AudioDeviceInfo for each available speaker. Raises: PlatformAudioError: If device enumeration fails. """ req = proto_ffi.FfiRequest() req.get_audio_devices.platform_audio_handle = self._ffi_handle.handle resp = FfiClient.instance.request(req) if resp.get_audio_devices.HasField("error") and resp.get_audio_devices.error: raise PlatformAudioError( f"Failed to enumerate playout devices: {resp.get_audio_devices.error}" ) return [AudioDeviceInfo._from_proto(d) for d in resp.get_audio_devices.playout_devices]Get available playout devices (speakers/headphones).
Returns
List of AudioDeviceInfo for each available speaker.
Raises
PlatformAudioError- If device enumeration fails.
def recording_devices(self) ‑> List[AudioDeviceInfo]-
Expand source code
def recording_devices(self) -> List[AudioDeviceInfo]: """Get available recording devices (microphones). Returns: List of AudioDeviceInfo for each available microphone. Raises: PlatformAudioError: If device enumeration fails. """ req = proto_ffi.FfiRequest() req.get_audio_devices.platform_audio_handle = self._ffi_handle.handle resp = FfiClient.instance.request(req) if resp.get_audio_devices.HasField("error") and resp.get_audio_devices.error: raise PlatformAudioError( f"Failed to enumerate recording devices: {resp.get_audio_devices.error}" ) return [AudioDeviceInfo._from_proto(d) for d in resp.get_audio_devices.recording_devices]Get available recording devices (microphones).
Returns
List of AudioDeviceInfo for each available microphone.
Raises
PlatformAudioError- If device enumeration fails.
def set_playout_device(self, device_id: str) ‑> None-
Expand source code
def set_playout_device(self, device_id: str) -> None: """Select the playout device (speaker) to use. Call this before connecting to a room to select which speaker to use for audio output. If not called, the system default speaker is used. Args: device_id: The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events. Raises: PlatformAudioError: If device selection fails. """ req = proto_ffi.FfiRequest() req.set_playout_device.platform_audio_handle = self._ffi_handle.handle req.set_playout_device.device_id = device_id resp = FfiClient.instance.request(req) if resp.set_playout_device.HasField("error") and resp.set_playout_device.error: raise PlatformAudioError( f"Failed to set playout device: {resp.set_playout_device.error}" )Select the playout device (speaker) to use.
Call this before connecting to a room to select which speaker to use for audio output. If not called, the system default speaker is used.
Args
device_id- The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events.
Raises
PlatformAudioError- If device selection fails.
def set_recording_device(self, device_id: str) ‑> None-
Expand source code
def set_recording_device(self, device_id: str) -> None: """Select the recording device (microphone) to use. Call this before creating audio tracks to select which microphone to use. If not called, the system default microphone is used. Args: device_id: The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events. Raises: PlatformAudioError: If device selection fails. """ req = proto_ffi.FfiRequest() req.set_recording_device.platform_audio_handle = self._ffi_handle.handle req.set_recording_device.device_id = device_id resp = FfiClient.instance.request(req) if resp.set_recording_device.HasField("error") and resp.set_recording_device.error: raise PlatformAudioError( f"Failed to set recording device: {resp.set_recording_device.error}" )Select the recording device (microphone) to use.
Call this before creating audio tracks to select which microphone to use. If not called, the system default microphone is used.
Args
device_id- The device ID (GUID) from AudioDeviceInfo.id. Use the ID rather than index for stable device selection across hot-plug events.
Raises
PlatformAudioError- If device selection fails.
class PlatformAudioError (*args, **kwargs)-
Expand source code
class PlatformAudioError(Exception): """Exception raised when PlatformAudio operations fail.""" passException raised when PlatformAudio operations fail.
Ancestors
- builtins.Exception
- builtins.BaseException
class PlatformAudioOptions (echo_cancellation: bool = True,
noise_suppression: bool = True,
auto_gain_control: bool = True,
prefer_hardware: bool = False)-
Expand source code
@dataclass class PlatformAudioOptions: """Audio processing options for PlatformAudio. These options configure WebRTC's audio processing pipeline when using PlatformAudio for microphone capture. Attributes: echo_cancellation: Enable acoustic echo cancellation (AEC) to remove echo from speaker playback. Default: True. noise_suppression: Enable noise suppression to remove background noise. Default: True. auto_gain_control: Enable automatic gain control to normalize audio levels. Default: True. prefer_hardware: Prefer hardware audio processing when available. - iOS: Uses Voice Processing I/O (VPIO) for low-latency processing - Android: Hardware support varies by device - Desktop: Generally not available Default: True on iOS, False on other platforms. """ echo_cancellation: bool = True noise_suppression: bool = True auto_gain_control: bool = True prefer_hardware: bool = False def _to_proto(self) -> proto_audio_frame.AudioSourceOptions: return proto_audio_frame.AudioSourceOptions( echo_cancellation=self.echo_cancellation, noise_suppression=self.noise_suppression, auto_gain_control=self.auto_gain_control, prefer_hardware=self.prefer_hardware, )Audio processing options for PlatformAudio.
These options configure WebRTC's audio processing pipeline when using PlatformAudio for microphone capture.
Attributes
echo_cancellation- Enable acoustic echo cancellation (AEC) to remove echo from speaker playback. Default: True.
noise_suppression- Enable noise suppression to remove background noise. Default: True.
auto_gain_control- Enable automatic gain control to normalize audio levels. Default: True.
prefer_hardware- Prefer hardware audio processing when available. - iOS: Uses Voice Processing I/O (VPIO) for low-latency processing - Android: Hardware support varies by device - Desktop: Generally not available Default: True on iOS, False on other platforms.
Instance variables
var auto_gain_control : boolvar echo_cancellation : boolvar noise_suppression : boolvar prefer_hardware : bool
class PlatformAudioSource (ffi_handle: FfiHandle, info: proto_audio_frame.AudioSourceInfo)-
Expand source code
class PlatformAudioSource: """Audio source backed by PlatformAudio (WebRTC ADM). This source captures audio from the selected microphone via the platform's Audio Device Module. Unlike AudioSource (synthetic mode), frames are captured and sent directly by the ADM - there is no `capture_frame()` method. Use this with LocalAudioTrack.create_audio_track() to publish microphone audio. Note: This class is created via PlatformAudio.create_audio_source() and should not be instantiated directly. Resource Management: Call `close()` when done to immediately release native resources. If not called, resources are released when the object is garbage collected, but GC timing is non-deterministic and may cause issues on some platforms (especially Windows) where audio device handles must be released promptly. """ def __init__(self, ffi_handle: FfiHandle, info: proto_audio_frame.AudioSourceInfo): self._ffi_handle = ffi_handle self._info = info @property def _handle(self) -> int: """Internal FFI handle for use with LocalAudioTrack.create_audio_track().""" return self._ffi_handle.handle def close(self) -> None: """Release the native audio source resources. Call this method when you are done using the audio source to immediately release the underlying native handle. This is especially important on Windows where audio device handles must be released promptly to avoid interfering with other audio operations. If `close()` is not called, resources will be released when the object is garbage collected. However, Python's garbage collection timing is non-deterministic, which can lead to: - Audio device contention if creating new audio sources before old ones are collected - Test failures when running multiple audio tests in sequence - Resource leaks in long-running applications that frequently create and discard audio sources It is safe to call `close()` multiple times; subsequent calls are no-ops. """ self._ffi_handle.dispose()Audio source backed by PlatformAudio (WebRTC ADM).
This source captures audio from the selected microphone via the platform's Audio Device Module. Unlike AudioSource (synthetic mode), frames are captured and sent directly by the ADM - there is no
capture_frame()method.Use this with LocalAudioTrack.create_audio_track() to publish microphone audio.
Note: This class is created via PlatformAudio.create_audio_source() and should not be instantiated directly.
Resource Management: Call
close()when done to immediately release native resources. If not called, resources are released when the object is garbage collected, but GC timing is non-deterministic and may cause issues on some platforms (especially Windows) where audio device handles must be released promptly.Methods
def close(self) ‑> None-
Expand source code
def close(self) -> None: """Release the native audio source resources. Call this method when you are done using the audio source to immediately release the underlying native handle. This is especially important on Windows where audio device handles must be released promptly to avoid interfering with other audio operations. If `close()` is not called, resources will be released when the object is garbage collected. However, Python's garbage collection timing is non-deterministic, which can lead to: - Audio device contention if creating new audio sources before old ones are collected - Test failures when running multiple audio tests in sequence - Resource leaks in long-running applications that frequently create and discard audio sources It is safe to call `close()` multiple times; subsequent calls are no-ops. """ self._ffi_handle.dispose()Release the native audio source resources.
Call this method when you are done using the audio source to immediately release the underlying native handle. This is especially important on Windows where audio device handles must be released promptly to avoid interfering with other audio operations.
If
close()is not called, resources will be released when the object is garbage collected. However, Python's garbage collection timing is non-deterministic, which can lead to: - Audio device contention if creating new audio sources before old ones are collected - Test failures when running multiple audio tests in sequence - Resource leaks in long-running applications that frequently create and discard audio sourcesIt is safe to call
close()multiple times; subsequent calls are no-ops.