-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathPlatformAudio.cs
More file actions
374 lines (334 loc) · 16.1 KB
/
PlatformAudio.cs
File metadata and controls
374 lines (334 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using LiveKit.Proto;
using LiveKit.Internal;
using LiveKit.Internal.FFIClients.Requests;
namespace LiveKit
{
#if UNITY_IOS && !UNITY_EDITOR
internal static class IOSAudioSessionHelper
{
/// <summary>
/// Configures the iOS audio session for VoIP/WebRTC.
/// Must be called before creating PlatformAudio.
/// </summary>
[DllImport("__Internal")]
internal static extern void LiveKit_ConfigureAudioSessionForVoIP();
/// <summary>
/// Restores the iOS audio session to ambient mode.
/// </summary>
[DllImport("__Internal")]
internal static extern void LiveKit_RestoreDefaultAudioSession();
}
#endif
/// <summary>
/// Information about an audio device (microphone or speaker).
/// </summary>
public struct AudioDevice
{
/// <summary>Device index (0-based). Note: indices can change when devices are added/removed.</summary>
public uint Index;
/// <summary>Device name as reported by the operating system.</summary>
public string Name;
/// <summary>
/// Platform-specific unique device identifier (GUID).
/// This is stable across device additions/removals and should be preferred
/// over index for device selection.
/// </summary>
public string Guid;
}
/// <summary>
/// Platform audio device management using WebRTC's Audio Device Module (ADM).
///
/// PlatformAudio provides access to the platform's audio devices (microphones and
/// speakers) and enables automatic audio capture and playback through WebRTC's ADM.
///
/// Key features:
/// - Echo cancellation (AEC)
/// - Automatic gain control (AGC)
/// - Noise suppression (NS)
/// - Automatic speaker playout for remote audio
///
/// Usage:
/// 1. Create a PlatformAudio instance (enables ADM)
/// 2. Optionally enumerate and select devices
/// 3. Create audio tracks using PlatformAudioSource
/// 4. Remote audio automatically plays through speakers
/// </summary>
/// <example>
/// <code>
/// // Create PlatformAudio (enables ADM)
/// var platformAudio = new PlatformAudio();
///
/// // Enumerate devices
/// var (recording, playout) = platformAudio.GetDevices();
/// foreach (var device in recording)
/// Debug.Log($"Mic {device.Index}: {device.Name}");
///
/// // Select devices
/// platformAudio.SetRecordingDevice(0);
/// platformAudio.SetPlayoutDevice(0);
///
/// // Create audio source and track
/// var source = new PlatformAudioSource(platformAudio);
/// var track = LocalAudioTrack.CreateAudioTrack("microphone", source, room);
///
/// // Publish track
/// await room.LocalParticipant.PublishTrack(track, options);
///
/// // Dispose when done
/// platformAudio.Dispose();
/// </code>
/// </example>
public sealed class PlatformAudio : IDisposable
{
internal readonly FfiHandle Handle;
private readonly PlatformAudioInfo _info;
private bool _disposed = false;
/// <summary>
/// Number of available recording (microphone) devices.
/// </summary>
public int RecordingDeviceCount => _info.RecordingDeviceCount;
/// <summary>
/// Number of available playout (speaker) devices.
/// </summary>
public int PlayoutDeviceCount => _info.PlayoutDeviceCount;
/// <summary>
/// Creates a new PlatformAudio instance, enabling the platform ADM.
///
/// This must be called before creating any PlatformAudioSource or connecting
/// to a room if you want automatic speaker playout for remote audio.
///
/// On iOS, this automatically configures the audio session for VoIP mode
/// (PlayAndRecord category with VoiceChat mode) to enable hardware echo
/// cancellation and microphone input.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the platform ADM could not be initialized (e.g., no audio devices,
/// missing permissions).
/// </exception>
public PlatformAudio()
{
#if UNITY_IOS && !UNITY_EDITOR
// Configure iOS audio session for VoIP before initializing WebRTC ADM.
// This sets PlayAndRecord category with VoiceChat mode for hardware AEC.
IOSAudioSessionHelper.LiveKit_ConfigureAudioSessionForVoIP();
#endif
using var request = FFIBridge.Instance.NewRequest<NewPlatformAudioRequest>();
using var response = request.Send();
FfiResponse res = response;
if (res.NewPlatformAudio.MessageCase == NewPlatformAudioResponse.MessageOneofCase.Error)
throw new InvalidOperationException($"Failed to create PlatformAudio: {res.NewPlatformAudio.Error}");
var platformAudio = res.NewPlatformAudio.PlatformAudio;
Handle = FfiHandle.FromOwnedHandle(platformAudio.Handle);
_info = platformAudio.Info;
Utils.Debug($"PlatformAudio created: {RecordingDeviceCount} recording devices, {PlayoutDeviceCount} playout devices");
}
/// <summary>
/// Gets the lists of available recording and playout devices.
/// </summary>
/// <returns>
/// A tuple containing:
/// - Recording: List of available microphones
/// - Playout: List of available speakers/headphones
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if device enumeration failed.
/// </exception>
public (List<AudioDevice> Recording, List<AudioDevice> Playout) GetDevices()
{
using var request = FFIBridge.Instance.NewRequest<GetAudioDevicesRequest>();
request.request.PlatformAudioHandle = (ulong)Handle.DangerousGetHandle();
using var response = request.Send();
FfiResponse res = response;
if (res.GetAudioDevices.HasError && !string.IsNullOrEmpty(res.GetAudioDevices.Error))
throw new InvalidOperationException($"Failed to get audio devices: {res.GetAudioDevices.Error}");
var recording = new List<AudioDevice>();
foreach (var device in res.GetAudioDevices.RecordingDevices)
{
recording.Add(new AudioDevice {
Index = device.Index,
Name = device.Name,
Guid = device.HasGuid ? device.Guid : null
});
}
var playout = new List<AudioDevice>();
foreach (var device in res.GetAudioDevices.PlayoutDevices)
{
playout.Add(new AudioDevice {
Index = device.Index,
Name = device.Name,
Guid = device.HasGuid ? device.Guid : null
});
}
return (recording, playout);
}
/// <summary>
/// Sets the recording device (microphone) by index.
///
/// Call this before creating audio tracks to select which microphone to use.
/// Device indices are 0-based and must be less than RecordingDeviceCount.
///
/// Note: Prefer SetRecordingDevice(string deviceId) for robust device selection across hot-plug events.
/// </summary>
/// <param name="index">Device index from GetDevices().Recording</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device index is invalid or the operation failed.
/// </exception>
public void SetRecordingDevice(uint index)
{
// Look up the device GUID by index
var (recording, _) = GetDevices();
if (index >= recording.Count)
throw new InvalidOperationException($"Recording device index {index} out of range (max: {recording.Count - 1})");
var deviceId = recording[(int)index].Guid;
// Note: On Android, devices don't have GUIDs - they're identified by index only.
// Android also only reports a single "default" microphone because the system
// automatically selects the best input source based on the audio mode.
// If GUID is empty, we pass an empty string which triggers index-0 fallback in native code.
SetRecordingDevice(deviceId ?? "");
Utils.Debug($"PlatformAudio: set recording device to index {index} (GUID: {(string.IsNullOrEmpty(deviceId) ? "<empty>" : deviceId)})");
}
/// <summary>
/// Sets the recording device (microphone) by device ID (GUID).
///
/// This is the preferred method for device selection as device IDs are stable
/// across device hot-plug events, unlike indices which can change.
/// </summary>
/// <param name="deviceId">Device ID/GUID from GetDevices().Recording[i].Guid</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device is not found or the operation failed.
/// </exception>
public void SetRecordingDevice(string deviceId)
{
using var request = FFIBridge.Instance.NewRequest<SetRecordingDeviceRequest>();
request.request.PlatformAudioHandle = (ulong)Handle.DangerousGetHandle();
request.request.DeviceId = deviceId;
using var response = request.Send();
FfiResponse res = response;
if (res.SetRecordingDevice.HasError && !string.IsNullOrEmpty(res.SetRecordingDevice.Error))
throw new InvalidOperationException($"Failed to set recording device: {res.SetRecordingDevice.Error}");
Utils.Debug($"PlatformAudio: set recording device to {deviceId}");
}
/// <summary>
/// Sets the recording device (microphone) by GUID.
/// </summary>
/// <param name="guid">Device GUID from GetDevices().Recording[i].Guid</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device is not found or the operation failed.
/// </exception>
[Obsolete("Use SetRecordingDevice(string deviceId) instead")]
public void SetRecordingDeviceByGuid(string guid) => SetRecordingDevice(guid);
/// <summary>
/// Sets the playout device (speaker/headphones) by index.
///
/// Call this before connecting to select which speaker to use for remote audio.
/// Device indices are 0-based and must be less than PlayoutDeviceCount.
///
/// Note: Prefer SetPlayoutDevice(string deviceId) for robust device selection across hot-plug events.
/// </summary>
/// <param name="index">Device index from GetDevices().Playout</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device index is invalid or the operation failed.
/// </exception>
public void SetPlayoutDevice(uint index)
{
// Look up the device GUID by index
var (_, playout) = GetDevices();
if (index >= playout.Count)
throw new InvalidOperationException($"Playout device index {index} out of range (max: {playout.Count - 1})");
var deviceId = playout[(int)index].Guid;
// Note: On Android, devices don't have GUIDs - they're identified by index only.
// Android also only reports a single "default" device because audio routing
// (speaker vs earpiece vs Bluetooth) is handled by the system via AudioManager,
// not through WebRTC device selection. Use Android's AudioManager API to switch outputs.
// If GUID is empty, we pass an empty string which triggers index-0 fallback in native code.
SetPlayoutDevice(deviceId ?? "");
Utils.Debug($"PlatformAudio: set playout device to index {index} (GUID: {(string.IsNullOrEmpty(deviceId) ? "<empty>" : deviceId)})");
}
/// <summary>
/// Sets the playout device (speaker/headphones) by device ID (GUID).
///
/// This is the preferred method for device selection as device IDs are stable
/// across device hot-plug events, unlike indices which can change.
/// </summary>
/// <param name="deviceId">Device ID/GUID from GetDevices().Playout[i].Guid</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device is not found or the operation failed.
/// </exception>
public void SetPlayoutDevice(string deviceId)
{
using var request = FFIBridge.Instance.NewRequest<SetPlayoutDeviceRequest>();
request.request.PlatformAudioHandle = (ulong)Handle.DangerousGetHandle();
request.request.DeviceId = deviceId;
using var response = request.Send();
FfiResponse res = response;
if (res.SetPlayoutDevice.HasError && !string.IsNullOrEmpty(res.SetPlayoutDevice.Error))
throw new InvalidOperationException($"Failed to set playout device: {res.SetPlayoutDevice.Error}");
Utils.Debug($"PlatformAudio: set playout device to {deviceId}");
}
/// <summary>
/// Sets the playout device (speaker/headphones) by GUID.
/// </summary>
/// <param name="guid">Device GUID from GetDevices().Playout[i].Guid</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device is not found or the operation failed.
/// </exception>
[Obsolete("Use SetPlayoutDevice(string deviceId) instead")]
public void SetPlayoutDeviceByGuid(string guid) => SetPlayoutDevice(guid);
/// <summary>
/// Starts recording from the microphone.
///
/// Recording is started automatically when PlatformAudio is created.
/// Use this to resume recording after calling StopRecording.
/// This turns on the system's recording privacy indicator (e.g., on macOS/iOS).
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the operation failed.
/// </exception>
public void StartRecording()
{
using var request = FFIBridge.Instance.NewRequest<StartRecordingRequest>();
request.request.PlatformAudioHandle = (ulong)Handle.DangerousGetHandle();
using var response = request.Send();
FfiResponse res = response;
if (res.StartRecording.HasError && !string.IsNullOrEmpty(res.StartRecording.Error))
throw new InvalidOperationException($"Failed to start recording: {res.StartRecording.Error}");
Utils.Debug("PlatformAudio: started recording");
}
/// <summary>
/// Stops recording from the microphone.
///
/// Use this to temporarily stop recording without disposing PlatformAudio.
/// This turns off the system's recording privacy indicator (e.g., on macOS/iOS).
/// Call StartRecording to resume recording.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the operation failed.
/// </exception>
public void StopRecording()
{
using var request = FFIBridge.Instance.NewRequest<StopRecordingRequest>();
request.request.PlatformAudioHandle = (ulong)Handle.DangerousGetHandle();
using var response = request.Send();
FfiResponse res = response;
if (res.StopRecording.HasError && !string.IsNullOrEmpty(res.StopRecording.Error))
throw new InvalidOperationException($"Failed to stop recording: {res.StopRecording.Error}");
Utils.Debug("PlatformAudio: stopped recording");
}
/// <summary>
/// Releases the PlatformAudio resources.
///
/// When disposed, the platform ADM may be disabled if this was the last
/// PlatformAudio instance.
/// </summary>
public void Dispose()
{
if (_disposed) return;
Handle.Dispose();
_disposed = true;
Utils.Debug("PlatformAudio disposed");
}
}
}