-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathPlatformAudio.cs
More file actions
390 lines (342 loc) · 17 KB
/
Copy pathPlatformAudio.cs
File metadata and controls
390 lines (342 loc) · 17 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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
using System;
using System.Collections;
using System.Collections.Generic;
using LiveKit.Proto;
using LiveKit.Internal;
using LiveKit.Internal.FFI.Requests;
#if UNITY_IOS && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
#if PLATFORM_ANDROID
using UnityEngine.Android;
#endif
using LiveKit.Internal.FFI;
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
public sealed class PlatformAudio : IDisposable
{
internal readonly FfiHandle Handle;
private readonly PlatformAudioInfo _info;
private bool _disposed = false;
#if UNITY_IOS && !UNITY_EDITOR
// Tracks live PlatformAudio instances so the iOS audio session is restored
// only when the last one is disposed (aligned with the native ADM ref-count).
private static int _instanceCount;
#endif
/// <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");
#if UNITY_IOS && !UNITY_EDITOR
// Count this instance only after successful construction so a failed
// ctor never leaves the counter stuck above zero.
System.Threading.Interlocked.Increment(ref _instanceCount);
#endif
}
/// <summary>
/// Gets the lists of available recording and playout devices.
///
/// Platform behavior:
/// - Desktop (Windows/macOS/Linux): returns the full list of microphones and
/// speakers reported by the OS. Devices can be selected with
/// <see cref="SetRecordingDevice(string)"/> / <see cref="SetPlayoutDevice(string)"/>.
/// - iOS and Android: returns a single placeholder entry at index 0 for each
/// list, representing the system's currently selected default input/output.
/// The OS owns audio routing on these platforms (AVAudioSession on iOS,
/// AudioManager on Android), so individual devices are not enumerated and
/// selecting one is a no-op (see <see cref="SetRecordingDevice(string)"/> /
/// <see cref="SetPlayoutDevice(string)"/>).
/// </summary>
/// <returns>
/// A tuple containing:
/// - Recording: List of available microphones (on iOS/Android, a single
/// placeholder for the OS default input)
/// - Playout: List of available speakers/headphones (on iOS/Android, a single
/// placeholder for the OS default output)
/// </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.
///
/// Convenience wrapper around <see cref="SetRecordingDevice(string)"/> that looks
/// up the GUID from <see cref="GetDevices"/>. Prefer the GUID overload for code
/// that persists a selection — indices can shift when devices are added/removed.
/// </summary>
/// <param name="index">Device index from GetDevices().Recording</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device index is out of range or the operation failed.
/// </exception>
public void SetRecordingDevice(uint index)
{
var (recording, _) = GetDevices();
if (index >= recording.Count)
throw new InvalidOperationException($"Recording device index {index} out of range (max: {recording.Count - 1})");
SetRecordingDevice(recording[(int)index].Guid ?? "");
}
/// <summary>
/// Sets the recording device (microphone) by device ID (GUID).
///
/// On Android and iOS this is a no-op in the native ADM: input routing is
/// governed by the OS (AVAudioSession on iOS, AudioManager on Android) and
/// the call is acknowledged but ignored. The method is still safe to call,
/// and the response carries no error. <see cref="GetDevices"/> only exposes a
/// single placeholder entry (index 0) for the OS default input on these
/// platforms, so there is nothing else to select.
/// </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 playout device (speaker/headphones) by index.
///
/// Convenience wrapper around <see cref="SetPlayoutDevice(string)"/> that looks
/// up the GUID from <see cref="GetDevices"/>. Prefer the GUID overload for code
/// that persists a selection — indices can shift when devices are added/removed.
/// </summary>
/// <param name="index">Device index from GetDevices().Playout</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the device index is out of range or the operation failed.
/// </exception>
public void SetPlayoutDevice(uint index)
{
var (_, playout) = GetDevices();
if (index >= playout.Count)
throw new InvalidOperationException($"Playout device index {index} out of range (max: {playout.Count - 1})");
SetPlayoutDevice(playout[(int)index].Guid ?? "");
}
/// <summary>
/// Sets the playout device (speaker/headphones) by device ID (GUID).
///
/// On Android and iOS this is a no-op in the native ADM: output routing is
/// governed by the OS (AVAudioSession on iOS, AudioManager on Android) and
/// the call is acknowledged but ignored. The method is still safe to call,
/// and the response carries no error. <see cref="GetDevices"/> only exposes a
/// single placeholder entry (index 0) for the OS default output on these
/// platforms, so there is nothing else to select.
/// </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>
/// 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 IEnumerator StartRecording()
{
#if PLATFORM_ANDROID
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
// Fire the system permission dialog and yield until the user resolves it.
// PermissionCallbacks delivers the result asynchronously from the Android OS;
// we poll the captured flag from this coroutine until one of the callbacks
// sets it. Without this gate, the WebRTC Android ADM would crash the process
// when AudioRecord fails to open due to the missing permission.
bool? granted = null;
var callbacks = new PermissionCallbacks();
callbacks.PermissionGranted += _ => granted = true;
callbacks.PermissionDenied += _ => granted = false;
callbacks.PermissionDeniedAndDontAskAgain += _ => granted = false;
Permission.RequestUserPermission(Permission.Microphone, callbacks);
while (granted == null)
yield return null;
if (granted == false)
throw new InvalidOperationException(
"Microphone permission denied by user; cannot start recording.");
}
#endif
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");
// Ensures this method is always a valid iterator even when the PLATFORM_ANDROID
// branch is compiled out (no `yield return` would otherwise be reachable on
// non-Android builds, which is a compile error for IEnumerator-returning methods).
yield break;
}
/// <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;
#if UNITY_IOS && !UNITY_EDITOR
// Restore the audio session Unity had before VoIP, but only once the
// last PlatformAudio is gone (the native ADM is likewise ref-counted).
if (System.Threading.Interlocked.Decrement(ref _instanceCount) == 0)
IOSAudioSessionHelper.LiveKit_RestoreDefaultAudioSession();
#endif
Utils.Debug("PlatformAudio disposed");
}
}
}