@mentra/acs-meeting 3.2.1-dev.311 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +15 -1
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +92 -21
- package/android/src/main/java/com/mentra/acsmeeting/LobbyAdmission.kt +2 -2
- package/android/src/main/java/com/mentra/acsmeeting/RemoteRoster.kt +3 -3
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +9 -7
- package/build/AcsMeeting.types.d.ts +3 -0
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +7 -0
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/build/AcsMeetingModule.web.d.ts +2 -0
- package/build/AcsMeetingModule.web.d.ts.map +1 -1
- package/build/AcsMeetingModule.web.js +3 -0
- package/build/AcsMeetingModule.web.js.map +1 -1
- package/ios/AcsFrameSender.swift +7 -4
- package/ios/AcsMeetingModule.swift +164 -30
- package/package.json +2 -2
- package/src/AcsMeeting.types.ts +3 -0
- package/src/AcsMeetingModule.ts +11 -1
- package/src/AcsMeetingModule.web.ts +8 -1
package/README.md
CHANGED
|
@@ -237,6 +237,7 @@ Audio and video control:
|
|
|
237
237
|
| Function | Notes |
|
|
238
238
|
|---|---|
|
|
239
239
|
| `setMuted(muted)` | Gates the uplink chain synchronously, then applies ACS policy |
|
|
240
|
+
| `setVideoEnabled(enabled)` | ACS `stopVideo`/`startVideo` on the virtual outgoing stream. Glasses source and preview tap keep running; `videoEnabled` resets to true per join |
|
|
240
241
|
| `setAudioSource(source)` | **No-op.** The source is locked for the call at join |
|
|
241
242
|
| `pushOutgoingPcm(base64, rate, ch)` | Synchronous `Function`, ~100 calls/s. Returns whether it entered the uplink |
|
|
242
243
|
| `updateVideoSource(whepUrl)` | WHEP only |
|
|
@@ -86,6 +86,7 @@ class AcsMeetingModule : Module() {
|
|
|
86
86
|
|
|
87
87
|
override fun definition() = ModuleDefinition {
|
|
88
88
|
Name("MentraAcsMeeting")
|
|
89
|
+
Function("supportsTeamsIdentity") { true }
|
|
89
90
|
// `onScopedNetworkLost` fires only for a hotspot that went away while we still wanted it: the
|
|
90
91
|
// scoped state machine drops the framework's `onLost` for a network we released ourselves, so a
|
|
91
92
|
// normal Leave or End cannot manufacture a mid-call network error.
|
|
@@ -268,7 +269,7 @@ class AcsMeetingModule : Module() {
|
|
|
268
269
|
// follows, because ACS re-binds its signalling when the network moves and keeps whatever
|
|
269
270
|
// route it got. Teardown's `leaveAndAwait` is what drops it, and only once a validated
|
|
270
271
|
// default internet exists — not `leaveScopedNetwork`, which used to unpin onto SoftAP.
|
|
271
|
-
meeting.prepareAgent(token, displayName)
|
|
272
|
+
meeting.prepareAgent(token, displayName, options["identityMode"] as? String ?: "guest")
|
|
272
273
|
meeting.snapshot()
|
|
273
274
|
}
|
|
274
275
|
}
|
|
@@ -326,6 +327,7 @@ class AcsMeetingModule : Module() {
|
|
|
326
327
|
video,
|
|
327
328
|
audioDelayMs,
|
|
328
329
|
origin,
|
|
330
|
+
identityMode = options["identityMode"] as? String ?: "guest",
|
|
329
331
|
bindIngestUnpinned = { bind -> withIngestUnpinned(bind) },
|
|
330
332
|
)
|
|
331
333
|
// Prefer the join snapshot: getState() can race a leave from a respawned miniapp
|
|
@@ -427,6 +429,18 @@ class AcsMeetingModule : Module() {
|
|
|
427
429
|
session?.setMuted(muted) ?: mapOf("state" to "idle", "muted" to muted)
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
AsyncFunction("setVideoEnabled") { enabled: Boolean, promise: Promise ->
|
|
433
|
+
val meeting = session
|
|
434
|
+
if (meeting == null) {
|
|
435
|
+
promise.reject("VIDEO_TOGGLE_FAILED", "No active meeting", null)
|
|
436
|
+
} else {
|
|
437
|
+
meeting.setVideoEnabled(enabled) { state, error ->
|
|
438
|
+
if (error != null) promise.reject("VIDEO_TOGGLE_FAILED", error.message, error)
|
|
439
|
+
else promise.resolve(state)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
430
444
|
AsyncFunction("setAudioSource") { source: String ->
|
|
431
445
|
session?.setAudioSource(source) ?: mapOf("state" to "idle", "muted" to false, "audioSource" to source)
|
|
432
446
|
}
|
|
@@ -12,7 +12,11 @@ import com.azure.android.communication.calling.AudioStreamState
|
|
|
12
12
|
import com.azure.android.communication.calling.AudioStreamType
|
|
13
13
|
import com.azure.android.communication.calling.CapabilitiesCallFeature
|
|
14
14
|
import com.azure.android.communication.calling.CapabilitiesChangedListener
|
|
15
|
-
import com.azure.android.communication.calling.
|
|
15
|
+
import com.azure.android.communication.calling.CommonCall
|
|
16
|
+
import com.azure.android.communication.calling.CommonCallAgent
|
|
17
|
+
import com.azure.android.communication.calling.TeamsCallAgent
|
|
18
|
+
import com.azure.android.communication.calling.TeamsCallAgentOptions
|
|
19
|
+
import com.azure.android.communication.calling.JoinTeamsCallOptions
|
|
16
20
|
import com.azure.android.communication.calling.CallAgent
|
|
17
21
|
import com.azure.android.communication.calling.CallAgentOptions
|
|
18
22
|
import com.azure.android.communication.calling.CallClient
|
|
@@ -70,6 +74,7 @@ import com.mentra.glassesmedia.source.OutgoingRateArm
|
|
|
70
74
|
import com.mentra.glassesmedia.source.PixelFormatArm
|
|
71
75
|
import com.mentra.glassesmedia.source.GlassesMediaController
|
|
72
76
|
import com.mentra.glassesmedia.network.ScopedSoftApNetwork
|
|
77
|
+
import com.mentra.glassesmedia.source.DecodedFrameTap
|
|
73
78
|
import com.mentra.glassesmedia.source.GlassesMediaSourceFactory
|
|
74
79
|
import com.mentra.glassesmedia.source.LocalWhipIngestSource
|
|
75
80
|
import com.mentra.acsmeeting.source.MeetingVideoSourceSpec
|
|
@@ -200,12 +205,14 @@ class AcsMeetingSession(
|
|
|
200
205
|
private var noNetworkListener: DiagnosticFlagChangedListener? = null
|
|
201
206
|
private var relaysListener: DiagnosticFlagChangedListener? = null
|
|
202
207
|
private var callClient: CallClient? = null
|
|
203
|
-
private var callAgent:
|
|
204
|
-
private var call:
|
|
208
|
+
private var callAgent: CommonCallAgent? = null
|
|
209
|
+
private var call: CommonCall? = null
|
|
205
210
|
private var audioOut: RawOutgoingAudioStream? = null
|
|
206
211
|
private var localOut: LocalOutgoingAudioStream? = null
|
|
207
212
|
private var audioIn: RawIncomingAudioStream? = null
|
|
208
213
|
private var videoOut: VirtualOutgoingVideoStream? = null
|
|
214
|
+
/** Whether ACS is sending [videoOut]. Only [setVideoEnabled] turns it off; every call starts on. */
|
|
215
|
+
@Volatile private var videoEnabled = true
|
|
209
216
|
@Volatile private var meetingUrl: String? = null
|
|
210
217
|
@Volatile private var phase = "idle"
|
|
211
218
|
@Volatile private var lastError: String? = null
|
|
@@ -265,13 +272,15 @@ class AcsMeetingSession(
|
|
|
265
272
|
* `createCallAgent` Future we stopped waiting on. ACS still finishes signing in; this is the only
|
|
266
273
|
* handle that can dispose that leftover agent before the next join.
|
|
267
274
|
*/
|
|
268
|
-
private var abandonedAgent: Future<
|
|
275
|
+
private var abandonedAgent: Future<out CommonCallAgent>? = null
|
|
269
276
|
/**
|
|
270
277
|
* Agent signed in before the glasses hotspot came up. SoftAP DNS cannot resolve ACS hosts, so
|
|
271
278
|
* `createCallAgent` after the scoped join stalls until the hotspot is torn down. Join reuses this
|
|
272
279
|
* instead of signing in again on the broken resolver.
|
|
273
280
|
*/
|
|
274
281
|
@Volatile private var agentPrepared = false
|
|
282
|
+
private var preparedToken: String? = null
|
|
283
|
+
private var preparedIdentityMode: String? = null
|
|
275
284
|
private val controller = SessionAudioController()
|
|
276
285
|
private val applier = AudioPolicyApplier(controller, scheduler) { Log.i(TAG, it) }
|
|
277
286
|
|
|
@@ -279,6 +288,7 @@ class AcsMeetingSession(
|
|
|
279
288
|
val result = mutableMapOf<String, Any>(
|
|
280
289
|
"state" to phase,
|
|
281
290
|
"muted" to muted.get(),
|
|
291
|
+
"videoEnabled" to videoEnabled,
|
|
282
292
|
"provider" to "acs-teams",
|
|
283
293
|
"audioSource" to audioSource,
|
|
284
294
|
"activeStream" to controller.readActive().name.lowercase(),
|
|
@@ -314,7 +324,7 @@ class AcsMeetingSession(
|
|
|
314
324
|
* hotspot too. Here nothing is torn down while we wait, and a cold sign-in on a slow AP measured
|
|
315
325
|
* 35 s on device — under the old 20 s cap that agent arrived just in time to be thrown away.
|
|
316
326
|
*/
|
|
317
|
-
fun prepareAgent(token: String, displayName: String
|
|
327
|
+
fun prepareAgent(token: String, displayName: String?, identityMode: String = "guest") {
|
|
318
328
|
val done = CountDownLatch(1)
|
|
319
329
|
val error = AtomicReference<Exception?>(null)
|
|
320
330
|
phase = "connecting"
|
|
@@ -332,8 +342,10 @@ class AcsMeetingSession(
|
|
|
332
342
|
callClient = CallClient()
|
|
333
343
|
val agentOptions = CallAgentOptions()
|
|
334
344
|
agentOptions.displayName = displayName ?: "Mentra Call"
|
|
335
|
-
callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, PREPARE_AGENT_WAIT_MS)
|
|
345
|
+
callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, identityMode, PREPARE_AGENT_WAIT_MS)
|
|
336
346
|
agentPrepared = true
|
|
347
|
+
preparedToken = token
|
|
348
|
+
preparedIdentityMode = identityMode
|
|
337
349
|
Log.i(TAG, "ACS call agent prepared before SoftAP")
|
|
338
350
|
SoftApTrace.stage(
|
|
339
351
|
"session_prepare_agent_end",
|
|
@@ -379,6 +391,7 @@ class AcsMeetingSession(
|
|
|
379
391
|
audioDelayMs: Int? = null,
|
|
380
392
|
/** `created` (Start) or `joined` (Join); see [callOrigin]. Diagnostic only. */
|
|
381
393
|
origin: String = "unknown",
|
|
394
|
+
identityMode: String = "guest",
|
|
382
395
|
/**
|
|
383
396
|
* Runs the WHIP listener bind, and exists so the caller can lift a process-wide network pin
|
|
384
397
|
* across exactly that call. Takes the block rather than being a pair of before/after hooks so
|
|
@@ -426,7 +439,7 @@ class AcsMeetingSession(
|
|
|
426
439
|
// the host and miniapp flash out of "joining" on every join.
|
|
427
440
|
// A SoftAP join that already signed in on cellular must keep that agent:
|
|
428
441
|
// recreating it on the glasses hotspot is the ACS_AGENT_TIMEOUT we just hit.
|
|
429
|
-
val reuseAgent = agentPrepared && callAgent != null
|
|
442
|
+
val reuseAgent = agentPrepared && callAgent != null && preparedToken == token && preparedIdentityMode == identityMode
|
|
430
443
|
leaveLocked(emitIdle = false, keepAgent = reuseAgent)
|
|
431
444
|
// After the teardown, because that teardown bumps the generation itself. Everything that
|
|
432
445
|
// moves it runs on this executor, so the value is stable for the rest of this join.
|
|
@@ -483,7 +496,7 @@ class AcsMeetingSession(
|
|
|
483
496
|
callClient = CallClient()
|
|
484
497
|
val agentOptions = CallAgentOptions()
|
|
485
498
|
agentOptions.displayName = displayName ?: "Mentra Call"
|
|
486
|
-
callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation)
|
|
499
|
+
callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, identityMode)
|
|
487
500
|
}
|
|
488
501
|
|
|
489
502
|
val videoOptions = RawOutgoingVideoStreamOptions()
|
|
@@ -632,7 +645,10 @@ class AcsMeetingSession(
|
|
|
632
645
|
bindIngestUnpinned {
|
|
633
646
|
media.attach(
|
|
634
647
|
video = { planes ->
|
|
635
|
-
|
|
648
|
+
// Preview first, deliberately: the tap must see frames ACS's pacing and
|
|
649
|
+
// readiness gates would otherwise hide, and it cannot delay or break this call.
|
|
650
|
+
DecodedFrameTap.offer(planes)
|
|
651
|
+
if (frameSender.sendPlanes(planes)) DecodedFrameTap.recordAcsSend()
|
|
636
652
|
},
|
|
637
653
|
pcm = { pcm, rate, channels -> feedOutgoingPcm(pcm, rate, channels) },
|
|
638
654
|
config = videoSource.toConfig(),
|
|
@@ -641,7 +657,12 @@ class AcsMeetingSession(
|
|
|
641
657
|
}
|
|
642
658
|
|
|
643
659
|
val locator = TeamsMeetingLinkLocator(teamsUrl)
|
|
644
|
-
val joined =
|
|
660
|
+
val joined = when (val agent = callAgent!!) {
|
|
661
|
+
is TeamsCallAgent -> agent.join(context, locator, JoinTeamsCallOptions()
|
|
662
|
+
.setOutgoingVideoOptions(ov).setOutgoingAudioOptions(oa).setIncomingAudioOptions(ia))
|
|
663
|
+
is CallAgent -> agent.join(context, locator, joinOptions)
|
|
664
|
+
else -> throw IllegalStateException("Unsupported meeting agent")
|
|
665
|
+
}
|
|
645
666
|
call = joined
|
|
646
667
|
roster.attach(joined)
|
|
647
668
|
joined.addOnStateChangedListener { pushCallState(joined.state) }
|
|
@@ -654,7 +675,8 @@ class AcsMeetingSession(
|
|
|
654
675
|
if (videoSource !is MeetingVideoSourceSpec.SoftAp) {
|
|
655
676
|
media.attach(
|
|
656
677
|
video = { planes ->
|
|
657
|
-
|
|
678
|
+
DecodedFrameTap.offer(planes)
|
|
679
|
+
if (frameSender.sendPlanes(planes)) DecodedFrameTap.recordAcsSend()
|
|
658
680
|
},
|
|
659
681
|
pcm = { pcm, rate, channels -> feedOutgoingPcm(pcm, rate, channels) },
|
|
660
682
|
config = if (synthetic) SourceConfig("", SourceKind.DIRECT) else videoSource.toConfig(),
|
|
@@ -886,6 +908,45 @@ class AcsMeetingSession(
|
|
|
886
908
|
return snap
|
|
887
909
|
}
|
|
888
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Stop or resume the camera Teams receives without leaving the call. The glasses source, the
|
|
913
|
+
* preview tap and the WHEP/WHIP transport keep running; [frameSender] drops frames while ACS
|
|
914
|
+
* reports the stream stopped. [videoEnabled] only moves once ACS accepts the change.
|
|
915
|
+
*/
|
|
916
|
+
fun setVideoEnabled(next: Boolean, complete: (Map<String, Any>?, Throwable?) -> Unit) {
|
|
917
|
+
val generation = joinGeneration.get()
|
|
918
|
+
executor.execute {
|
|
919
|
+
val active = call
|
|
920
|
+
val stream = videoOut
|
|
921
|
+
if (active == null || stream == null || joinGeneration.get() != generation) {
|
|
922
|
+
complete(null, IllegalStateException("No active meeting"))
|
|
923
|
+
return@execute
|
|
924
|
+
}
|
|
925
|
+
if (videoEnabled == next) {
|
|
926
|
+
complete(snapshot(), null)
|
|
927
|
+
return@execute
|
|
928
|
+
}
|
|
929
|
+
val result = runCatching {
|
|
930
|
+
val change = if (next) active.startVideo(context, stream) else active.stopVideo(context, stream)
|
|
931
|
+
change.get(VIDEO_TOGGLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
932
|
+
}
|
|
933
|
+
if (call !== active || joinGeneration.get() != generation) {
|
|
934
|
+
complete(null, IllegalStateException("The meeting ended before the camera changed"))
|
|
935
|
+
return@execute
|
|
936
|
+
}
|
|
937
|
+
result.exceptionOrNull()?.let { error ->
|
|
938
|
+
Log.e(TAG, "setVideoEnabled=$next failed", error)
|
|
939
|
+
complete(null, error)
|
|
940
|
+
return@execute
|
|
941
|
+
}
|
|
942
|
+
Log.i(TAG, "setVideoEnabled=$next")
|
|
943
|
+
videoEnabled = next
|
|
944
|
+
val snap = snapshot()
|
|
945
|
+
onState(snap)
|
|
946
|
+
complete(snap, null)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
889
950
|
fun setAudioSource(source: String): Map<String, Any> {
|
|
890
951
|
val parsed = AcsAudioPolicy.parseSource(source)
|
|
891
952
|
if (parsed == null) {
|
|
@@ -1224,7 +1285,7 @@ class AcsMeetingSession(
|
|
|
1224
1285
|
""
|
|
1225
1286
|
}
|
|
1226
1287
|
|
|
1227
|
-
private fun attachMediaStats(joined:
|
|
1288
|
+
private fun attachMediaStats(joined: CommonCall) {
|
|
1228
1289
|
detachMediaStats()
|
|
1229
1290
|
mediaStatsReports.set(0)
|
|
1230
1291
|
lastWireSizeKey = null
|
|
@@ -1443,7 +1504,7 @@ class AcsMeetingSession(
|
|
|
1443
1504
|
* "sender's video is frozen" guidance points at these diagnostics instead:
|
|
1444
1505
|
* they are the only send-side network signal the SDK exposes.
|
|
1445
1506
|
*/
|
|
1446
|
-
private fun attachDiagnostics(joined:
|
|
1507
|
+
private fun attachDiagnostics(joined: CommonCall) {
|
|
1447
1508
|
detachDiagnostics()
|
|
1448
1509
|
try {
|
|
1449
1510
|
val feature = joined.feature(Features.LOCAL_USER_DIAGNOSTICS) as LocalUserDiagnosticsCallFeature
|
|
@@ -1638,7 +1699,7 @@ class AcsMeetingSession(
|
|
|
1638
1699
|
* The capability that matters is `HANG_UP_FOR_EVERYONE`. It can flip mid-call — a presenter role
|
|
1639
1700
|
* granted or removed — so the listener stays attached rather than reading once at connect.
|
|
1640
1701
|
*/
|
|
1641
|
-
private fun attachCapabilities(joined:
|
|
1702
|
+
private fun attachCapabilities(joined: CommonCall) {
|
|
1642
1703
|
detachCapabilities()
|
|
1643
1704
|
try {
|
|
1644
1705
|
val feature = joined.feature(Features.CAPABILITIES)
|
|
@@ -1750,16 +1811,17 @@ class AcsMeetingSession(
|
|
|
1750
1811
|
credential: CommunicationTokenCredential,
|
|
1751
1812
|
options: CallAgentOptions,
|
|
1752
1813
|
generation: Int,
|
|
1814
|
+
identityMode: String,
|
|
1753
1815
|
waitMs: Long = CALL_AGENT_WAIT_MS,
|
|
1754
|
-
):
|
|
1816
|
+
): CommonCallAgent {
|
|
1755
1817
|
disposeAbandonedAgent(waitMs = ABANDONED_AGENT_REJOIN_WAIT_MS)
|
|
1756
1818
|
return try {
|
|
1757
|
-
awaitCallAgent(client, credential, options, generation, waitMs)
|
|
1819
|
+
awaitCallAgent(client, credential, options, generation, identityMode, waitMs)
|
|
1758
1820
|
} catch (error: Exception) {
|
|
1759
1821
|
if (!AbandonedCallAgent.isExistingAgentError(error)) throw error
|
|
1760
1822
|
Log.w(TAG, "createCallAgent hit leftover identity; disposing abandoned agent and retrying")
|
|
1761
1823
|
disposeAbandonedAgent(waitMs = ABANDONED_AGENT_REJOIN_WAIT_MS)
|
|
1762
|
-
awaitCallAgent(client, credential, options, generation, waitMs)
|
|
1824
|
+
awaitCallAgent(client, credential, options, generation, identityMode, waitMs)
|
|
1763
1825
|
}
|
|
1764
1826
|
}
|
|
1765
1827
|
|
|
@@ -1768,11 +1830,16 @@ class AcsMeetingSession(
|
|
|
1768
1830
|
credential: CommunicationTokenCredential,
|
|
1769
1831
|
options: CallAgentOptions,
|
|
1770
1832
|
generation: Int,
|
|
1833
|
+
identityMode: String,
|
|
1771
1834
|
waitMs: Long,
|
|
1772
|
-
):
|
|
1835
|
+
): CommonCallAgent {
|
|
1773
1836
|
val startedAt = SystemClock.elapsedRealtime()
|
|
1774
1837
|
SoftApTrace.stage("session_call_agent_wait", "waitMs" to waitMs, "generation" to generation)
|
|
1775
|
-
val pending =
|
|
1838
|
+
val pending: Future<out CommonCallAgent> = when (identityMode) {
|
|
1839
|
+
"guest" -> client.createCallAgent(context, credential, options)
|
|
1840
|
+
"teams-user" -> client.createTeamsCallAgent(context, credential, TeamsCallAgentOptions())
|
|
1841
|
+
else -> throw IllegalArgumentException("Unsupported meeting identity mode")
|
|
1842
|
+
}
|
|
1776
1843
|
val agent = try {
|
|
1777
1844
|
pending.get(waitMs, TimeUnit.MILLISECONDS)
|
|
1778
1845
|
} catch (timeout: TimeoutException) {
|
|
@@ -1815,7 +1882,7 @@ class AcsMeetingSession(
|
|
|
1815
1882
|
* Gives up after a bounded number of sweeps: by then the process has either got the agent or the
|
|
1816
1883
|
* future is never completing, and an endless timer is its own leak.
|
|
1817
1884
|
*/
|
|
1818
|
-
private fun sweepLateAgent(pending: Future<
|
|
1885
|
+
private fun sweepLateAgent(pending: Future<out CommonCallAgent>, sweep: Int) {
|
|
1819
1886
|
if (sweep >= LATE_AGENT_SWEEPS) {
|
|
1820
1887
|
Log.w(TAG, "abandoned call agent never completed; stopping sweep")
|
|
1821
1888
|
// Giving up on the sweep is giving up on disposing that agent. It is a bounded leak by
|
|
@@ -1992,6 +2059,8 @@ class AcsMeetingSession(
|
|
|
1992
2059
|
callClient = null
|
|
1993
2060
|
call = null
|
|
1994
2061
|
callAgent = null
|
|
2062
|
+
preparedToken = null
|
|
2063
|
+
preparedIdentityMode = null
|
|
1995
2064
|
agentPrepared = false
|
|
1996
2065
|
}
|
|
1997
2066
|
media.stop()
|
|
@@ -1999,6 +2068,7 @@ class AcsMeetingSession(
|
|
|
1999
2068
|
localOut = null
|
|
2000
2069
|
audioIn = null
|
|
2001
2070
|
videoOut = null
|
|
2071
|
+
videoEnabled = true
|
|
2002
2072
|
outgoingReady.set(false)
|
|
2003
2073
|
muted.set(false)
|
|
2004
2074
|
hangUpForEveryone = CapabilityStatus()
|
|
@@ -2075,6 +2145,7 @@ class AcsMeetingSession(
|
|
|
2075
2145
|
private const val TAG = "ACS-SPIKE"
|
|
2076
2146
|
const val GLASSES_REQUIRES_UNMUTED_TRANSPORT = true
|
|
2077
2147
|
private const val ROSTER_COALESCE_MS = 150L
|
|
2148
|
+
private const val VIDEO_TOGGLE_TIMEOUT_MS = 10_000L
|
|
2078
2149
|
private const val MEDIA_RESTART_BASE_MS = 1_000L
|
|
2079
2150
|
private const val MEDIA_RESTART_MAX_MS = 10_000L
|
|
2080
2151
|
|
|
@@ -2151,7 +2222,7 @@ class AcsMeetingSession(
|
|
|
2151
2222
|
null -> null
|
|
2152
2223
|
}
|
|
2153
2224
|
|
|
2154
|
-
private fun describeEndReason(call:
|
|
2225
|
+
private fun describeEndReason(call: CommonCall?): Map<String, Any?> {
|
|
2155
2226
|
if (call == null) return mapOf("hasCall" to false)
|
|
2156
2227
|
return try {
|
|
2157
2228
|
val getter = call.javaClass.methods.firstOrNull {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
package com.mentra.acsmeeting
|
|
2
2
|
|
|
3
|
-
import com.azure.android.communication.calling.
|
|
3
|
+
import com.azure.android.communication.calling.CommonCall
|
|
4
4
|
import com.azure.android.communication.calling.CallState
|
|
5
5
|
import com.azure.android.communication.calling.ParticipantState
|
|
6
6
|
|
|
7
7
|
/** Admit one current lobby guest without blocking the session executor or changing meeting policy. */
|
|
8
8
|
internal fun admitLobbyParticipant(
|
|
9
|
-
active:
|
|
9
|
+
active: CommonCall,
|
|
10
10
|
participantId: String,
|
|
11
11
|
allowed: Boolean,
|
|
12
12
|
isCurrent: () -> Boolean,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.mentra.acsmeeting
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import com.azure.android.communication.calling.
|
|
4
|
+
import com.azure.android.communication.calling.CommonCall
|
|
5
5
|
import com.azure.android.communication.calling.ParticipantState
|
|
6
6
|
import com.azure.android.communication.calling.ParticipantsUpdatedListener
|
|
7
7
|
import com.azure.android.communication.calling.PropertyChangedListener
|
|
@@ -21,11 +21,11 @@ class RemoteRoster(
|
|
|
21
21
|
val listener: PropertyChangedListener,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
private var call:
|
|
24
|
+
private var call: CommonCall? = null
|
|
25
25
|
private var updatedListener: ParticipantsUpdatedListener? = null
|
|
26
26
|
private val tracked = ConcurrentHashMap<String, Tracked>()
|
|
27
27
|
|
|
28
|
-
fun attach(joined:
|
|
28
|
+
fun attach(joined: CommonCall) {
|
|
29
29
|
detach()
|
|
30
30
|
call = joined
|
|
31
31
|
val listener = ParticipantsUpdatedListener { event ->
|
|
@@ -103,20 +103,21 @@ class AcsFrameSender(
|
|
|
103
103
|
|
|
104
104
|
fun isReady(): Boolean = stream.get() != null && running.get() && format.get() != null
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/** True when the frame was submitted to ACS; false when a readiness, size, pacing or busy gate dropped it. */
|
|
107
|
+
fun sendPlanes(planes: I420Planes): Boolean {
|
|
107
108
|
val out = stream.get()
|
|
108
109
|
if (out == null || !running.get()) {
|
|
109
110
|
stats.onDropNotStarted()
|
|
110
|
-
return
|
|
111
|
+
return false
|
|
111
112
|
}
|
|
112
113
|
val negotiated = format.get()
|
|
113
114
|
if (negotiated == null) {
|
|
114
115
|
stats.onDropNotStarted()
|
|
115
|
-
return
|
|
116
|
+
return false
|
|
116
117
|
}
|
|
117
118
|
if (negotiated.width != planes.width || negotiated.height != planes.height) {
|
|
118
119
|
stats.onDropSize()
|
|
119
|
-
return
|
|
120
|
+
return false
|
|
120
121
|
}
|
|
121
122
|
if (!planes.planesReadable()) {
|
|
122
123
|
stats.onDropMalformed()
|
|
@@ -125,11 +126,11 @@ class AcsFrameSender(
|
|
|
125
126
|
"P5 send plane too small ${planes.width}x${planes.height} " +
|
|
126
127
|
"stride=${planes.strideY}/${planes.strideU}/${planes.strideV}",
|
|
127
128
|
)
|
|
128
|
-
return
|
|
129
|
+
return false
|
|
129
130
|
}
|
|
130
131
|
if (!pacer.tryAdmit(FramePacer.intervalNs(negotiated.framesPerSecond))) {
|
|
131
132
|
stats.onDropPaced()
|
|
132
|
-
return
|
|
133
|
+
return false
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
val prepared = prepareSend(planes, negotiated)
|
|
@@ -139,7 +140,7 @@ class AcsFrameSender(
|
|
|
139
140
|
if (!gate.tryAcquire()) {
|
|
140
141
|
stats.onDropBusy()
|
|
141
142
|
releaseSendBuffers(prepared, planes)
|
|
142
|
-
return
|
|
143
|
+
return false
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
val seq = sendSeq.incrementAndGet()
|
|
@@ -230,6 +231,7 @@ class AcsFrameSender(
|
|
|
230
231
|
gate.release()
|
|
231
232
|
releaseSendBuffers(prepared, planes)
|
|
232
233
|
}
|
|
234
|
+
return submitted
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
private data class PreparedSend(
|
|
@@ -19,6 +19,8 @@ export type AcsMeetingParticipant = {
|
|
|
19
19
|
export type AcsMeetingState = {
|
|
20
20
|
state: MeetingPhase;
|
|
21
21
|
muted: boolean;
|
|
22
|
+
/** Whether ACS is sending the glasses camera. Resets to true on every join. */
|
|
23
|
+
videoEnabled?: boolean;
|
|
22
24
|
error?: string;
|
|
23
25
|
meetingUrl?: string;
|
|
24
26
|
provider?: "acs-teams";
|
|
@@ -45,6 +47,7 @@ export type AcsOutgoingVideo = {
|
|
|
45
47
|
export type AcsMeetingJoinOptions = {
|
|
46
48
|
meetingUrl: string;
|
|
47
49
|
token: string;
|
|
50
|
+
identityMode?: "guest" | "teams-user";
|
|
48
51
|
whepUrl?: string;
|
|
49
52
|
videoSource?: {
|
|
50
53
|
type: "whep";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeeting.types.d.ts","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAA;AAEnG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,CAAA;AAChD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAC1D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAC3D;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE3E,MAAM,MAAM,0BAA0B,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,CAAA;AAEhH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,0BAA0B,CAAA;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,CAAA;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mFAAmF;IACnF,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,CAAA;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,yDAAyD;IACzD,KAAK,CAAC,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,aAAa,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAClD,CAAA"}
|
|
1
|
+
{"version":3,"file":"AcsMeeting.types.d.ts","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAA;AAEnG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,CAAA;AAChD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAC1D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAC3D;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE3E,MAAM,MAAM,0BAA0B,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,CAAA;AAEhH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,0BAA0B,CAAA;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,CAAA;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mFAAmF;IACnF,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,CAAA;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,yDAAyD;IACzD,KAAK,CAAC,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,aAAa,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAClD,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeeting.types.js","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"","sourcesContent":["export type MeetingPhase = \"idle\" | \"connecting\" | \"lobby\" | \"connected\" | \"disconnected\" | \"error\"\n\nexport type AcsAudioSource = \"glasses\" | \"phone\"\nexport type AcsActiveStream = \"none\" | \"virtual\" | \"local\"\nexport type AcsAudioSafety = \"safe\" | \"degraded\" | \"unsafe\"\n/**\n * Health of the glasses WHEP subscription feeding the call. `failed` = ICE dropped or\n * the WHEP endpoint went away while the ACS call may still be `connected`. Native\n * rebuilds it on its own with backoff; the host can force one via `restartVideoSource`.\n */\nexport type AcsMediaSourceState = \"idle\" | \"connecting\" | \"live\" | \"failed\"\n\nexport type AcsMeetingParticipantState = \"idle\" | \"connecting\" | \"connected\" | \"lobby\" | \"hold\" | \"disconnected\"\n\nexport type AcsMeetingParticipant = {\n id: string\n displayName: string | null\n state: AcsMeetingParticipantState\n isMuted: boolean\n isSpeaking: boolean\n}\n\nexport type AcsMeetingState = {\n state: MeetingPhase\n muted: boolean\n error?: string\n meetingUrl?: string\n provider?: \"acs-teams\"\n audioSource?: AcsAudioSource\n activeStream?: AcsActiveStream\n audioSafety?: AcsAudioSafety\n mediaSource?: AcsMediaSourceState\n /** Native receiver diagnostic, including the reason a local WHIP offer was rejected. */\n mediaSourceReason?: string\n /** ACS disconnect diagnostics, emitted when the final disconnected state arrives. */\n endReason_code?: number\n endReason_subcode?: number\n /** Local WHIP endpoint, available after a SoftAP join or `rebindSoftApIngest`. */\n ingestUrl?: string\n /** Remote roster, including participant state, mute state and speaking updates. */\n participants?: AcsMeetingParticipant[]\n}\n\nexport type AcsOutgoingVideo = {\n width: number\n height: number\n fps: number\n maxBitrateBps: number\n}\n\nexport type AcsMeetingJoinOptions = {\n meetingUrl: string\n token: string\n whepUrl?: string\n videoSource?: {type: \"whep\"; url: string} | {type: \"softap\"; bindAddress: string}\n displayName?: string\n /** \"glasses\" sends WHEP PCM. \"phone\" uses the ACS local mic (handset or BT). */\n audioSource?: AcsAudioSource\n /**\n * Hold outgoing PCM this many ms before ACS. SoftAP+LC3 sets this in the host\n * because BLE audio reaches the phone before SoftAP video.\n */\n audioDelayMs?: number\n /** Dump WHEP PCM to a WAV in cache for P4 verification. */\n dumpPcmWav?: boolean\n /** When omitted, native keeps 1280×720@15 / 2.5 Mbps. */\n video?: AcsOutgoingVideo\n}\n\nexport type AcsIncomingPcmEvent = {\n base64: string\n sampleRate: number\n channels: number\n}\n\nexport type AcsMeetingModuleEvents = {\n onState: (state: AcsMeetingState) => void\n onIncomingPcm: (pcm: AcsIncomingPcmEvent) => void\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AcsMeeting.types.js","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"","sourcesContent":["export type MeetingPhase = \"idle\" | \"connecting\" | \"lobby\" | \"connected\" | \"disconnected\" | \"error\"\n\nexport type AcsAudioSource = \"glasses\" | \"phone\"\nexport type AcsActiveStream = \"none\" | \"virtual\" | \"local\"\nexport type AcsAudioSafety = \"safe\" | \"degraded\" | \"unsafe\"\n/**\n * Health of the glasses WHEP subscription feeding the call. `failed` = ICE dropped or\n * the WHEP endpoint went away while the ACS call may still be `connected`. Native\n * rebuilds it on its own with backoff; the host can force one via `restartVideoSource`.\n */\nexport type AcsMediaSourceState = \"idle\" | \"connecting\" | \"live\" | \"failed\"\n\nexport type AcsMeetingParticipantState = \"idle\" | \"connecting\" | \"connected\" | \"lobby\" | \"hold\" | \"disconnected\"\n\nexport type AcsMeetingParticipant = {\n id: string\n displayName: string | null\n state: AcsMeetingParticipantState\n isMuted: boolean\n isSpeaking: boolean\n}\n\nexport type AcsMeetingState = {\n state: MeetingPhase\n muted: boolean\n /** Whether ACS is sending the glasses camera. Resets to true on every join. */\n videoEnabled?: boolean\n error?: string\n meetingUrl?: string\n provider?: \"acs-teams\"\n audioSource?: AcsAudioSource\n activeStream?: AcsActiveStream\n audioSafety?: AcsAudioSafety\n mediaSource?: AcsMediaSourceState\n /** Native receiver diagnostic, including the reason a local WHIP offer was rejected. */\n mediaSourceReason?: string\n /** ACS disconnect diagnostics, emitted when the final disconnected state arrives. */\n endReason_code?: number\n endReason_subcode?: number\n /** Local WHIP endpoint, available after a SoftAP join or `rebindSoftApIngest`. */\n ingestUrl?: string\n /** Remote roster, including participant state, mute state and speaking updates. */\n participants?: AcsMeetingParticipant[]\n}\n\nexport type AcsOutgoingVideo = {\n width: number\n height: number\n fps: number\n maxBitrateBps: number\n}\n\nexport type AcsMeetingJoinOptions = {\n meetingUrl: string\n token: string\n identityMode?: \"guest\" | \"teams-user\"\n whepUrl?: string\n videoSource?: {type: \"whep\"; url: string} | {type: \"softap\"; bindAddress: string}\n displayName?: string\n /** \"glasses\" sends WHEP PCM. \"phone\" uses the ACS local mic (handset or BT). */\n audioSource?: AcsAudioSource\n /**\n * Hold outgoing PCM this many ms before ACS. SoftAP+LC3 sets this in the host\n * because BLE audio reaches the phone before SoftAP video.\n */\n audioDelayMs?: number\n /** Dump WHEP PCM to a WAV in cache for P4 verification. */\n dumpPcmWav?: boolean\n /** When omitted, native keeps 1280×720@15 / 2.5 Mbps. */\n video?: AcsOutgoingVideo\n}\n\nexport type AcsIncomingPcmEvent = {\n base64: string\n sampleRate: number\n channels: number\n}\n\nexport type AcsMeetingModuleEvents = {\n onState: (state: AcsMeetingState) => void\n onIncomingPcm: (pcm: AcsIncomingPcmEvent) => void\n}\n"]}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { NativeModule } from "expo";
|
|
2
2
|
import type { AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState } from "./AcsMeeting.types";
|
|
3
3
|
declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {
|
|
4
|
+
supportsTeamsIdentity(): boolean;
|
|
4
5
|
join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>;
|
|
5
6
|
/** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */
|
|
6
7
|
prepareAgent(options: {
|
|
7
8
|
token: string;
|
|
8
9
|
displayName?: string;
|
|
10
|
+
identityMode?: "guest" | "teams-user";
|
|
9
11
|
}): Promise<AcsMeetingState>;
|
|
10
12
|
leave(): Promise<void>;
|
|
11
13
|
admitParticipant?(participantId: string): Promise<void>;
|
|
@@ -19,6 +21,11 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
19
21
|
completed: boolean;
|
|
20
22
|
}>;
|
|
21
23
|
setMuted(muted: boolean): Promise<AcsMeetingState>;
|
|
24
|
+
/**
|
|
25
|
+
* Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses
|
|
26
|
+
* media source and the preview tap stay up; frames are dropped before ACS while stopped.
|
|
27
|
+
*/
|
|
28
|
+
setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>;
|
|
22
29
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
23
30
|
updateVideoSource(whepUrl: string): Promise<void>;
|
|
24
31
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,KAAK,EAAC,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;AAEtG,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IAC/E,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAC9D,iFAAiF;IACjF,YAAY,CAAC,OAAO,EAAE;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,KAAK,EAAC,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;AAEtG,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IAC/E,qBAAqB,IAAI,OAAO;IAChC,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAC9D,iFAAiF;IACjF,YAAY,CAAC,OAAO,EAAE;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;KACtC,GAAG,OAAO,CAAC,eAAe,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,gBAAgB,CAAC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACvD;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAClD;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAC3D,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IACrE,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACjD,wEAAwE;IACxE,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IACnC;;;;OAIG;IACH,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;IACrC,4EAA4E;IAC5E,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACpE,uFAAuF;IACvF,4BAA4B,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACjG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1C,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IACnC,uBAAuB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IACzC,+BAA+B,CAAC,IAAI,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,CAAC;IACjG,qEAAqE;IACrE,kBAAkB,IAAI,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;IACnE,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC;CACrC;;AAED,wBAA8E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AAiDtD,eAAe,mBAAmB,CAAyB,kBAAkB,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport type {AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState} from \"./AcsMeeting.types\"\n\ndeclare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {\n supportsTeamsIdentity(): boolean\n join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>\n /** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */\n prepareAgent(options: {\n token: string\n displayName?: string\n identityMode?: \"guest\" | \"teams-user\"\n }): Promise<AcsMeetingState>\n leave(): Promise<void>\n admitParticipant?(participantId: string): Promise<void>\n /**\n * Leave, and resolve only once the hang-up, the agent disposal, and the network releases have\n * finished. Use this explicit barrier across platforms; Android `leave()` only queues cleanup.\n */\n leaveAndAwait(options: {timeoutMs: number}): Promise<{completed: boolean}>\n setMuted(muted: boolean): Promise<AcsMeetingState>\n /**\n * Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses\n * media source and the preview tap stay up; frames are dropped before ACS while stopped.\n */\n setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>\n setAudioSource(source: \"glasses\" | \"phone\"): Promise<AcsMeetingState>\n updateVideoSource(whepUrl: string): Promise<void>\n /** Force a WHEP rebuild on the current URL (phone changed networks). */\n restartVideoSource(): Promise<void>\n /**\n * SoftAP: destroy the current ingest listener generation and bind a new one.\n * Resolves with the new URL the glasses must publish to. Rejects rather than\n * reuse a stale listener.\n */\n rebindSoftApIngest(): Promise<string>\n /** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */\n joinScopedNetwork(ssid: string, passphrase: string): Promise<string>\n /** iOS: verify DHCP against the gateway advertised by the glasses before resolving. */\n joinScopedNetworkWithGateway?(ssid: string, passphrase: string, gateway: string): Promise<string>\n beginTrace(traceId: string): Promise<void>\n leaveScopedNetwork(): Promise<void>\n cancelScopedNetworkJoin?(): Promise<void>\n awaitDefaultNetworkAfterHotspot?(): Promise<{usable: boolean; detail: string; transport: string}>\n /** SoftAP: TCP-probe the hotspot gateway over the scoped network. */\n probeScopedGateway(): Promise<{reachable: boolean; detail: string}>\n getState(): Promise<AcsMeetingState>\n}\n\nexport default requireNativeModule<AcsMeetingNativeModule>(\"MentraAcsMeeting\")\n"]}
|
|
@@ -4,6 +4,7 @@ declare const _default: {
|
|
|
4
4
|
prepareAgent(_options: {
|
|
5
5
|
token: string;
|
|
6
6
|
displayName?: string;
|
|
7
|
+
identityMode?: "guest" | "teams-user";
|
|
7
8
|
}): Promise<AcsMeetingState>;
|
|
8
9
|
leave(): Promise<void>;
|
|
9
10
|
leaveAndAwait(_options: {
|
|
@@ -12,6 +13,7 @@ declare const _default: {
|
|
|
12
13
|
completed: boolean;
|
|
13
14
|
}>;
|
|
14
15
|
setMuted(_muted: boolean): Promise<AcsMeetingState>;
|
|
16
|
+
setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState>;
|
|
15
17
|
setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
16
18
|
updateVideoSource(_whepUrl: string): Promise<void>;
|
|
17
19
|
restartVideoSource(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.web.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;;mBAOvD,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;2BAGxC;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.web.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;;mBAOvD,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;2BAGxC;QAC3B,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;KACtC,GAAG,OAAO,CAAC,eAAe,CAAC;aAGb,OAAO,CAAC,IAAI,CAAC;4BAGE;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;qBAG1D,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;8BAGzB,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;4BAGpC,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;gCAG1C,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;0BAG5B,OAAO,CAAC,IAAI,CAAC;0BAGb,OAAO,CAAC,MAAM,CAAC;6BAGZ,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;0BAGhD,OAAO,CAAC,IAAI,CAAC;0BACb,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;gBAGvD,OAAO,CAAC,eAAe,CAAC;;;;;;AA1C5C,wBAiDC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.web.js","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAEA,SAAS,WAAW;IAClB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;AACxD,CAAC;AAED,eAAe;IACb,KAAK,CAAC,IAAI,CAAC,QAA+B;QACxC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.web.js","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAEA,SAAS,WAAW;IAClB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;AACxD,CAAC;AAED,eAAe;IACb,KAAK,CAAC,IAAI,CAAC,QAA+B;QACxC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,QAIlB;QACC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,KAAK;QACT,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,QAA6B;QAC/C,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,MAAe;QAC5B,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,eAAe,CAAC,QAAiB;QACrC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,OAA4B;QAC/C,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,QAAgB;QACtC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB;QACtB,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB;QACtB,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,KAAa,EAAE,WAAmB;QACxD,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB,KAAmB,CAAC;IAC5C,KAAK,CAAC,kBAAkB;QACtB,OAAO,EAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,EAAC,CAAA;IAC/D,CAAC;IACD,KAAK,CAAC,QAAQ;QACZ,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;IACtC,CAAC;IACD,WAAW;QACT,OAAO,EAAC,MAAM,KAAI,CAAC,EAAC,CAAA;IACtB,CAAC;IACD,eAAe,KAAI,CAAC;CACrB,CAAA","sourcesContent":["import type {AcsMeetingJoinOptions, AcsMeetingState} from \"./AcsMeeting.types\"\n\nfunction unavailable(): never {\n throw new Error(\"ACS meeting is not available on web\")\n}\n\nexport default {\n async join(_options: AcsMeetingJoinOptions): Promise<AcsMeetingState> {\n unavailable()\n },\n async prepareAgent(_options: {\n token: string\n displayName?: string\n identityMode?: \"guest\" | \"teams-user\"\n }): Promise<AcsMeetingState> {\n unavailable()\n },\n async leave(): Promise<void> {\n unavailable()\n },\n async leaveAndAwait(_options: {timeoutMs: number}): Promise<{completed: boolean}> {\n unavailable()\n },\n async setMuted(_muted: boolean): Promise<AcsMeetingState> {\n unavailable()\n },\n async setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState> {\n unavailable()\n },\n async setAudioSource(_source: \"glasses\" | \"phone\"): Promise<AcsMeetingState> {\n unavailable()\n },\n async updateVideoSource(_whepUrl: string): Promise<void> {\n unavailable()\n },\n async restartVideoSource(): Promise<void> {\n unavailable()\n },\n async rebindSoftApIngest(): Promise<string> {\n unavailable()\n },\n async joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string> {\n unavailable()\n },\n async leaveScopedNetwork(): Promise<void> {},\n async probeScopedGateway(): Promise<{reachable: boolean; detail: string}> {\n return {reachable: false, detail: \"no scoped network on web\"}\n },\n async getState(): Promise<AcsMeetingState> {\n return {state: \"idle\", muted: false}\n },\n addListener() {\n return {remove() {}}\n },\n removeListeners() {},\n}\n"]}
|
package/ios/AcsFrameSender.swift
CHANGED
|
@@ -20,21 +20,23 @@ final class AcsFrameSender: NSObject {
|
|
|
20
20
|
stream.delegate = self
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/// True when the frame was handed to ACS; false when readiness, pacing or the send gate dropped it.
|
|
24
|
+
@discardableResult
|
|
25
|
+
func send(_ pixelBuffer: CVPixelBuffer) -> Bool {
|
|
24
26
|
stateLock.lock()
|
|
25
27
|
guard running, let stream else {
|
|
26
28
|
stateLock.unlock()
|
|
27
|
-
return
|
|
29
|
+
return false
|
|
28
30
|
}
|
|
29
31
|
let fps = stream.format.framesPerSecond
|
|
30
32
|
let now = CFAbsoluteTimeGetCurrent()
|
|
31
33
|
if lastSent > 0, now - lastSent < 1.0 / Double(max(fps, 1)) {
|
|
32
34
|
stateLock.unlock()
|
|
33
|
-
return
|
|
35
|
+
return false
|
|
34
36
|
}
|
|
35
37
|
guard let token = sendGate.acquire() else {
|
|
36
38
|
stateLock.unlock()
|
|
37
|
-
return
|
|
39
|
+
return false
|
|
38
40
|
}
|
|
39
41
|
lastSent = now
|
|
40
42
|
stateLock.unlock()
|
|
@@ -48,6 +50,7 @@ final class AcsFrameSender: NSObject {
|
|
|
48
50
|
sendGate.release(token)
|
|
49
51
|
if let error { NSLog("ACS-SPIKE send frame failed: \(error)") }
|
|
50
52
|
}
|
|
53
|
+
return true
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
func detach() {
|
|
@@ -38,11 +38,12 @@ public class AcsMeetingModule: Module {
|
|
|
38
38
|
|
|
39
39
|
public func definition() -> ModuleDefinition {
|
|
40
40
|
Name("MentraAcsMeeting")
|
|
41
|
+
Function("supportsTeamsIdentity") { true }
|
|
41
42
|
Events("onState", "onIncomingPcm", "onScopedNetworkLost", "onScopedNetworkProgress")
|
|
42
43
|
|
|
43
44
|
AsyncFunction("prepareAgent") { (options: [String: Any], promise: Promise) in
|
|
44
45
|
let token = try requireString(options, "token")
|
|
45
|
-
self.meetingSession().prepareAgent(token: token, displayName: options["displayName"] as? String) { result in
|
|
46
|
+
self.meetingSession().prepareAgent(token: token, displayName: options["displayName"] as? String, identityMode: options["identityMode"] as? String ?? "guest") { result in
|
|
46
47
|
switch result {
|
|
47
48
|
case let .success(state): promise.resolve(state)
|
|
48
49
|
case let .failure(error): promise.reject(error)
|
|
@@ -59,7 +60,7 @@ public class AcsMeetingModule: Module {
|
|
|
59
60
|
}
|
|
60
61
|
let video = try parseAcsOutgoingVideo(options["video"])
|
|
61
62
|
self.meetingSession().join(
|
|
62
|
-
token: token, meetingUrl: meetingUrl, sourceConfig: source,
|
|
63
|
+
token: token, identityMode: options["identityMode"] as? String ?? "guest", meetingUrl: meetingUrl, sourceConfig: source,
|
|
63
64
|
displayName: options["displayName"] as? String,
|
|
64
65
|
dumpWav: options["dumpPcmWav"] as? Bool ?? false,
|
|
65
66
|
audioSource: options["audioSource"] as? String ?? "glasses", video: video
|
|
@@ -163,6 +164,19 @@ public class AcsMeetingModule: Module {
|
|
|
163
164
|
session.setMuted(muted) { promise.resolve($0) }
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
AsyncFunction("setVideoEnabled") { (enabled: Bool, promise: Promise) in
|
|
168
|
+
guard let session = self.session else {
|
|
169
|
+
promise.reject(AcsMeetingError("No active meeting"))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
session.setVideoEnabled(enabled) { result in
|
|
173
|
+
switch result {
|
|
174
|
+
case let .success(state): promise.resolve(state)
|
|
175
|
+
case let .failure(error): promise.reject(error)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
AsyncFunction("setAudioSource") { (source: String, promise: Promise) in
|
|
167
181
|
guard let session = self.session else {
|
|
168
182
|
promise.resolve(["state": "idle", "muted": false, "audioSource": source]); return
|
|
@@ -235,16 +249,20 @@ final class AcsMeetingSession {
|
|
|
235
249
|
private lazy var applier = AudioPolicyApplier(controller: controller, scheduler: scheduler) { NSLog("ACS-SPIKE \($0)") }
|
|
236
250
|
private var phase = "idle"
|
|
237
251
|
private var muted = false
|
|
252
|
+
/// Whether ACS is sending `videoStream`. Only `setVideoEnabled` turns it off; every call starts on.
|
|
253
|
+
private var videoEnabled = true
|
|
254
|
+
private var videoStream: VirtualOutgoingVideoStream?
|
|
238
255
|
private var meetingUrl: String?
|
|
239
256
|
private var lastError: String?
|
|
240
257
|
private var callEndReason: (code: Int, subcode: Int)?
|
|
241
258
|
private var callClient: CallClient?
|
|
242
|
-
private var callAgent:
|
|
243
|
-
private var call:
|
|
259
|
+
private var callAgent: CommonCallAgent?
|
|
260
|
+
private var call: CommonCall?
|
|
244
261
|
private var media: DecodedGlassesMediaSource?
|
|
245
262
|
private var sourceConfig = SourceConfig(url: "")
|
|
246
|
-
private var preparedAgent:
|
|
263
|
+
private var preparedAgent: CommonCallAgent?
|
|
247
264
|
private var preparedClient: CallClient?
|
|
265
|
+
private var preparedIdentityMode: String?
|
|
248
266
|
private var preparedToken: String?
|
|
249
267
|
private var pendingJoin: ((Result<[String: Any], Error>) -> Void)?
|
|
250
268
|
private var pendingPrepare: ((Result<[String: Any], Error>) -> Void)?
|
|
@@ -281,6 +299,11 @@ final class AcsMeetingSession {
|
|
|
281
299
|
onMuteChange: { [weak self] call in self?.handleCallMuteChange(call) },
|
|
282
300
|
onRosterChange: { [weak self] call in self?.refreshRoster(call) }
|
|
283
301
|
)
|
|
302
|
+
private lazy var teamsCallDelegateProxy = AcsTeamsCallDelegateProxy(
|
|
303
|
+
onStateChange: { [weak self] call in self?.handleCallStateChange(call) },
|
|
304
|
+
onMuteChange: { [weak self] call in self?.handleCallMuteChange(call) },
|
|
305
|
+
onRosterChange: { [weak self] call in self?.refreshRoster(call) }
|
|
306
|
+
)
|
|
284
307
|
private var remoteParticipants: [ObjectIdentifier: RemoteParticipant] = [:]
|
|
285
308
|
private lazy var participantDelegateProxy = AcsParticipantDelegateProxy { [weak self] participant in
|
|
286
309
|
self?.queue.async {
|
|
@@ -308,6 +331,7 @@ final class AcsMeetingSession {
|
|
|
308
331
|
var result: [String: Any] = [
|
|
309
332
|
"state": phase,
|
|
310
333
|
"muted": muted,
|
|
334
|
+
"videoEnabled": videoEnabled,
|
|
311
335
|
"provider": "acs-teams",
|
|
312
336
|
"audioSource": audioSource,
|
|
313
337
|
"activeStream": controller.readActive().rawValue,
|
|
@@ -342,7 +366,7 @@ final class AcsMeetingSession {
|
|
|
342
366
|
return result
|
|
343
367
|
}
|
|
344
368
|
|
|
345
|
-
func prepareAgent(token: String, displayName: String?, completion: @escaping (Result<[String: Any], Error>) -> Void) {
|
|
369
|
+
func prepareAgent(token: String, displayName: String?, identityMode: String = "guest", completion: @escaping (Result<[String: Any], Error>) -> Void) {
|
|
346
370
|
queue.async {
|
|
347
371
|
guard self.cleanup.wait(timeout: .now()) == .success, self.pendingPrepare == nil, self.pendingJoin == nil, self.call == nil, self.media == nil else {
|
|
348
372
|
completion(.failure(AcsMeetingError("Previous call cleanup is still pending"))); return
|
|
@@ -356,7 +380,7 @@ final class AcsMeetingSession {
|
|
|
356
380
|
let credential = try CommunicationTokenCredential(token: token)
|
|
357
381
|
let options = CallAgentOptions()
|
|
358
382
|
options.displayName = displayName ?? "Mentra Call"
|
|
359
|
-
client
|
|
383
|
+
createMeetingAgent(client: client, credential: credential, guestOptions: options, identityMode: identityMode) { agent, error in
|
|
360
384
|
self.queue.async {
|
|
361
385
|
guard self.joinGeneration == generation, self.pendingPrepare != nil else { agent?.dispose(); return }
|
|
362
386
|
let reply = self.pendingPrepare
|
|
@@ -364,6 +388,7 @@ final class AcsMeetingSession {
|
|
|
364
388
|
if let agent, error == nil {
|
|
365
389
|
self.preparedAgent = agent
|
|
366
390
|
self.preparedToken = token
|
|
391
|
+
self.preparedIdentityMode = identityMode
|
|
367
392
|
reply?(.success(self.snapshotLocked()))
|
|
368
393
|
} else {
|
|
369
394
|
agent?.dispose()
|
|
@@ -384,7 +409,7 @@ final class AcsMeetingSession {
|
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
411
|
|
|
387
|
-
func join(token: String, meetingUrl: String, sourceConfig: SourceConfig, displayName: String?, dumpWav: Bool,
|
|
412
|
+
func join(token: String, identityMode: String = "guest", meetingUrl: String, sourceConfig: SourceConfig, displayName: String?, dumpWav: Bool,
|
|
388
413
|
audioSource: String = "glasses", video: AcsOutgoingVideo = .hd,
|
|
389
414
|
completion: @escaping (Result<[String: Any], Error>) -> Void)
|
|
390
415
|
{
|
|
@@ -392,7 +417,7 @@ final class AcsMeetingSession {
|
|
|
392
417
|
guard self.cleanup.wait(timeout: .now()) == .success, self.pendingPrepare == nil, self.pendingJoin == nil, self.call == nil, self.media == nil else {
|
|
393
418
|
completion(.failure(AcsMeetingError("Previous call cleanup is still pending"))); return
|
|
394
419
|
}
|
|
395
|
-
let prepared = self.preparedToken == token ? self.preparedAgent : nil
|
|
420
|
+
let prepared = self.preparedToken == token && self.preparedIdentityMode == identityMode ? self.preparedAgent : nil
|
|
396
421
|
let preparedClient = self.preparedClient
|
|
397
422
|
if prepared != nil { self.preparedAgent = nil; self.preparedClient = nil; self.preparedToken = nil }
|
|
398
423
|
self.leaveLocked(emitIdle: false)
|
|
@@ -404,7 +429,7 @@ final class AcsMeetingSession {
|
|
|
404
429
|
self.lastError = nil
|
|
405
430
|
self.callEndReason = nil
|
|
406
431
|
self.emit("connecting")
|
|
407
|
-
let useAgent: (
|
|
432
|
+
let useAgent: (CommonCallAgent) -> Void = { agent in
|
|
408
433
|
do { try self.joinWithAgentLocked(agent, generation: generation, meetingUrl: meetingUrl,
|
|
409
434
|
sourceConfig: sourceConfig, dumpWav: dumpWav, video: video) } catch { self.failJoinLocked(error, generation: generation) }
|
|
410
435
|
}
|
|
@@ -421,7 +446,7 @@ final class AcsMeetingSession {
|
|
|
421
446
|
options.displayName = displayName ?? "Mentra Call"
|
|
422
447
|
let agentStartedAt = ProcessInfo.processInfo.systemUptime
|
|
423
448
|
NSLog("ACS-SPIKE iOS agent creation started generation=\(generation)")
|
|
424
|
-
client
|
|
449
|
+
createMeetingAgent(client: client, credential: credential, guestOptions: options, identityMode: identityMode) { agent, error in
|
|
425
450
|
self.queue.async {
|
|
426
451
|
let elapsedMs = Int((ProcessInfo.processInfo.systemUptime - agentStartedAt) * 1000)
|
|
427
452
|
NSLog("ACS-SPIKE iOS agent creation completed generation=\(generation) elapsedMs=\(elapsedMs) success=\(agent != nil && error == nil)")
|
|
@@ -444,7 +469,7 @@ final class AcsMeetingSession {
|
|
|
444
469
|
}
|
|
445
470
|
|
|
446
471
|
private func joinWithAgentLocked(
|
|
447
|
-
_ agent:
|
|
472
|
+
_ agent: CommonCallAgent,
|
|
448
473
|
generation: UInt64,
|
|
449
474
|
meetingUrl: String,
|
|
450
475
|
sourceConfig: SourceConfig,
|
|
@@ -463,6 +488,7 @@ final class AcsMeetingSession {
|
|
|
463
488
|
let videoOptions = RawOutgoingVideoStreamOptions()
|
|
464
489
|
videoOptions.formats = [videoFormat]
|
|
465
490
|
let videoStream = VirtualOutgoingVideoStream(videoStreamOptions: videoOptions)
|
|
491
|
+
self.videoStream = videoStream
|
|
466
492
|
frameSender = AcsFrameSender()
|
|
467
493
|
frameSender.attach(videoStream)
|
|
468
494
|
|
|
@@ -523,14 +549,14 @@ final class AcsMeetingSession {
|
|
|
523
549
|
let locator = TeamsMeetingLinkLocator(meetingLink: meetingUrl)
|
|
524
550
|
// Cancellation reserves cleanup and retains the agent until the late join result
|
|
525
551
|
// can be hung up. The retirement deadline still handles an SDK callback that is lost.
|
|
526
|
-
let joinRetirement = CallJoinRetirement<
|
|
552
|
+
let joinRetirement = CallJoinRetirement<CommonCall>(group: cleanup, queue: queue, dispose: { agent.dispose() }) { call, finished in
|
|
527
553
|
call.hangUp(options: nil) { error in
|
|
528
554
|
if let error { NSLog("ACS-SPIKE cancelled join hangUp failed: \(error)") }
|
|
529
555
|
finished()
|
|
530
556
|
}
|
|
531
557
|
}
|
|
532
558
|
cancelPendingCallJoin = { joinRetirement.cancel() }
|
|
533
|
-
agent
|
|
559
|
+
joinMeeting(agent: agent, locator: locator, guestOptions: joinOptions) { call, error in
|
|
534
560
|
self.queue.async {
|
|
535
561
|
guard joinRetirement.receive(call) else { return }
|
|
536
562
|
guard self.callAgent === agent, self.joinGeneration == generation else {
|
|
@@ -559,7 +585,7 @@ final class AcsMeetingSession {
|
|
|
559
585
|
}
|
|
560
586
|
|
|
561
587
|
private func finishJoinLocked(
|
|
562
|
-
_ call:
|
|
588
|
+
_ call: CommonCall,
|
|
563
589
|
generation: UInt64,
|
|
564
590
|
sourceConfig: SourceConfig,
|
|
565
591
|
dumpWav: Bool,
|
|
@@ -571,7 +597,8 @@ final class AcsMeetingSession {
|
|
|
571
597
|
return
|
|
572
598
|
}
|
|
573
599
|
self.call = call
|
|
574
|
-
call
|
|
600
|
+
(call as? Call)?.delegate = callDelegateProxy
|
|
601
|
+
(call as? TeamsCall)?.delegate = teamsCallDelegateProxy
|
|
575
602
|
refreshRosterLocked(call)
|
|
576
603
|
attachCapabilities(call)
|
|
577
604
|
|
|
@@ -581,7 +608,12 @@ final class AcsMeetingSession {
|
|
|
581
608
|
self?.feedOutgoingPcm(pcm, sampleRate: rate, channels: channels, generation: generation)
|
|
582
609
|
}
|
|
583
610
|
let source: DecodedGlassesMediaSource = sourceConfig.kind == .softap ? LocalWhipIngestSource() : WhepVideoSource()
|
|
584
|
-
source.onFrame = { [frameSender] buffer in
|
|
611
|
+
source.onFrame = { [frameSender] buffer in
|
|
612
|
+
// Preview first, deliberately: the tap must see frames the ACS sender's pacing and
|
|
613
|
+
// readiness gates would otherwise hide, and it cannot delay or break this call.
|
|
614
|
+
DecodedFrameTap.shared.offer(buffer)
|
|
615
|
+
if frameSender.send(buffer) { DecodedFrameTap.shared.recordAcsSend() }
|
|
616
|
+
}
|
|
585
617
|
source.onPcm = { [weak self] pcm, rate, channels in
|
|
586
618
|
self?.feedOutgoingPcm(pcm, sampleRate: rate, channels: channels, generation: generation)
|
|
587
619
|
}
|
|
@@ -717,6 +749,46 @@ final class AcsMeetingSession {
|
|
|
717
749
|
}
|
|
718
750
|
}
|
|
719
751
|
|
|
752
|
+
/// Stop or resume the camera Teams receives without leaving the call. The glasses source, the
|
|
753
|
+
/// preview tap and the WHEP/WHIP transport keep running; `AcsFrameSender` drops frames while
|
|
754
|
+
/// ACS reports the stream stopped. `videoEnabled` only moves once ACS accepts the change.
|
|
755
|
+
func setVideoEnabled(_ next: Bool, completion: @escaping (Result<[String: Any], Error>) -> Void) {
|
|
756
|
+
queue.async {
|
|
757
|
+
guard let call = self.call, let stream = self.videoStream else {
|
|
758
|
+
completion(.failure(AcsMeetingError("No active meeting")))
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
guard self.videoEnabled != next else {
|
|
762
|
+
completion(.success(self.snapshotLocked()))
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
let generation = self.joinGeneration
|
|
766
|
+
let finished: (Error?) -> Void = { error in
|
|
767
|
+
self.queue.async {
|
|
768
|
+
guard self.joinGeneration == generation, self.call === call else {
|
|
769
|
+
completion(.failure(AcsMeetingError("The meeting ended before the camera changed")))
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
if let error {
|
|
773
|
+
NSLog("ACS-SPIKE setVideoEnabled=\(next) failed: \(error)")
|
|
774
|
+
completion(.failure(error))
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
NSLog("ACS-SPIKE setVideoEnabled=\(next)")
|
|
778
|
+
self.videoEnabled = next
|
|
779
|
+
let snapshot = self.snapshotLocked()
|
|
780
|
+
self.onState(snapshot)
|
|
781
|
+
completion(.success(snapshot))
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if next {
|
|
785
|
+
call.startVideo(stream: stream, completionHandler: finished)
|
|
786
|
+
} else {
|
|
787
|
+
call.stopVideo(stream: stream, completionHandler: finished)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
720
792
|
func setAudioSource(_ source: String, completion: @escaping ([String: Any]) -> Void) {
|
|
721
793
|
queue.async {
|
|
722
794
|
if AcsAudioPolicy.parseSource(source) == nil {
|
|
@@ -845,7 +917,7 @@ final class AcsMeetingSession {
|
|
|
845
917
|
promoted to presenter mid-call), and an End button that never turns on is the same bug as one
|
|
846
918
|
that lies about what it does.
|
|
847
919
|
*/
|
|
848
|
-
private func attachCapabilities(_ call:
|
|
920
|
+
private func attachCapabilities(_ call: CommonCall) {
|
|
849
921
|
let feature = call.feature(Features.capabilities)
|
|
850
922
|
capabilitiesFeature = feature
|
|
851
923
|
feature.delegate = capabilitiesDelegateProxy
|
|
@@ -976,7 +1048,8 @@ final class AcsMeetingSession {
|
|
|
976
1048
|
let leavingAgent = callAgent
|
|
977
1049
|
let cancelJoin = cancelPendingCallJoin
|
|
978
1050
|
cancelPendingCallJoin = nil
|
|
979
|
-
leavingCall?.delegate = nil
|
|
1051
|
+
(leavingCall as? Call)?.delegate = nil
|
|
1052
|
+
(leavingCall as? TeamsCall)?.delegate = nil
|
|
980
1053
|
if let leavingCall {
|
|
981
1054
|
// CallAgent.dispose releases all local SDK resources. The retirement deadline
|
|
982
1055
|
// also disposes the agent if hangUp loses its callback, before opening the barrier.
|
|
@@ -1004,6 +1077,8 @@ final class AcsMeetingSession {
|
|
|
1004
1077
|
pcmBridge = nil
|
|
1005
1078
|
outgoingReady = false
|
|
1006
1079
|
muted = false
|
|
1080
|
+
videoEnabled = true
|
|
1081
|
+
videoStream = nil
|
|
1007
1082
|
audioSource = "glasses"
|
|
1008
1083
|
lastSafety = .degraded
|
|
1009
1084
|
meetingUrl = nil
|
|
@@ -1017,7 +1092,7 @@ final class AcsMeetingSession {
|
|
|
1017
1092
|
}
|
|
1018
1093
|
}
|
|
1019
1094
|
|
|
1020
|
-
fileprivate func currentCall() ->
|
|
1095
|
+
fileprivate func currentCall() -> CommonCall? {
|
|
1021
1096
|
call
|
|
1022
1097
|
}
|
|
1023
1098
|
|
|
@@ -1029,14 +1104,14 @@ final class AcsMeetingSession {
|
|
|
1029
1104
|
phoneMic.setEnabled(enabled)
|
|
1030
1105
|
}
|
|
1031
1106
|
|
|
1032
|
-
private func handleCallStateChange(_ changedCall:
|
|
1107
|
+
private func handleCallStateChange(_ changedCall: CommonCall) {
|
|
1033
1108
|
queue.async {
|
|
1034
1109
|
guard self.call === changedCall, self.lastError == nil else { return }
|
|
1035
1110
|
self.refreshCallStateLocked(changedCall)
|
|
1036
1111
|
}
|
|
1037
1112
|
}
|
|
1038
1113
|
|
|
1039
|
-
private func refreshCallStateLocked(_ changedCall:
|
|
1114
|
+
private func refreshCallStateLocked(_ changedCall: CommonCall) {
|
|
1040
1115
|
switch changedCall.state {
|
|
1041
1116
|
case .connecting: emit("connecting")
|
|
1042
1117
|
case .inLobby: emit("lobby")
|
|
@@ -1071,14 +1146,14 @@ final class AcsMeetingSession {
|
|
|
1071
1146
|
}
|
|
1072
1147
|
}
|
|
1073
1148
|
|
|
1074
|
-
private func handleCallMuteChange(_ changedCall:
|
|
1149
|
+
private func handleCallMuteChange(_ changedCall: CommonCall) {
|
|
1075
1150
|
queue.async {
|
|
1076
1151
|
guard self.call === changedCall, self.lastError == nil else { return }
|
|
1077
1152
|
self.applyAudioPolicyOnQueue("outgoing-audio-state")
|
|
1078
1153
|
}
|
|
1079
1154
|
}
|
|
1080
1155
|
|
|
1081
|
-
private func refreshRoster(_ changedCall:
|
|
1156
|
+
private func refreshRoster(_ changedCall: CommonCall) {
|
|
1082
1157
|
queue.async {
|
|
1083
1158
|
guard self.call === changedCall else { return }
|
|
1084
1159
|
self.refreshRosterLocked(changedCall)
|
|
@@ -1086,7 +1161,7 @@ final class AcsMeetingSession {
|
|
|
1086
1161
|
}
|
|
1087
1162
|
}
|
|
1088
1163
|
|
|
1089
|
-
private func refreshRosterLocked(_ changedCall:
|
|
1164
|
+
private func refreshRosterLocked(_ changedCall: CommonCall) {
|
|
1090
1165
|
dispatchPrecondition(condition: .onQueue(queue))
|
|
1091
1166
|
let current = Dictionary(uniqueKeysWithValues: changedCall.remoteParticipants.map {
|
|
1092
1167
|
(ObjectIdentifier($0), $0)
|
|
@@ -1209,12 +1284,12 @@ final class SessionAudioController: AudioStreamController {
|
|
|
1209
1284
|
}
|
|
1210
1285
|
|
|
1211
1286
|
private final class AcsCallDelegateProxy: NSObject, CallDelegate {
|
|
1212
|
-
private let onStateChange: (
|
|
1213
|
-
private let onMuteChange: (
|
|
1214
|
-
private let onRosterChange: (
|
|
1287
|
+
private let onStateChange: (CommonCall) -> Void
|
|
1288
|
+
private let onMuteChange: (CommonCall) -> Void
|
|
1289
|
+
private let onRosterChange: (CommonCall) -> Void
|
|
1215
1290
|
|
|
1216
|
-
init(onStateChange: @escaping (
|
|
1217
|
-
onRosterChange: @escaping (
|
|
1291
|
+
init(onStateChange: @escaping (CommonCall) -> Void, onMuteChange: @escaping (CommonCall) -> Void,
|
|
1292
|
+
onRosterChange: @escaping (CommonCall) -> Void)
|
|
1218
1293
|
{
|
|
1219
1294
|
self.onStateChange = onStateChange
|
|
1220
1295
|
self.onMuteChange = onMuteChange
|
|
@@ -1234,6 +1309,32 @@ private final class AcsCallDelegateProxy: NSObject, CallDelegate {
|
|
|
1234
1309
|
}
|
|
1235
1310
|
}
|
|
1236
1311
|
|
|
1312
|
+
private final class AcsTeamsCallDelegateProxy: NSObject, TeamsCallDelegate {
|
|
1313
|
+
private let onStateChange: (CommonCall) -> Void
|
|
1314
|
+
private let onMuteChange: (CommonCall) -> Void
|
|
1315
|
+
private let onRosterChange: (CommonCall) -> Void
|
|
1316
|
+
|
|
1317
|
+
init(onStateChange: @escaping (CommonCall) -> Void, onMuteChange: @escaping (CommonCall) -> Void,
|
|
1318
|
+
onRosterChange: @escaping (CommonCall) -> Void)
|
|
1319
|
+
{
|
|
1320
|
+
self.onStateChange = onStateChange
|
|
1321
|
+
self.onMuteChange = onMuteChange
|
|
1322
|
+
self.onRosterChange = onRosterChange
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
func teamsCall(_ call: TeamsCall, didChangeState _: PropertyChangedEventArgs) {
|
|
1326
|
+
onStateChange(call)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
func teamsCall(_ call: TeamsCall, didUpdateOutgoingAudioState _: PropertyChangedEventArgs) {
|
|
1330
|
+
onMuteChange(call)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
func teamsCall(_ call: TeamsCall, didUpdateRemoteParticipant _: ParticipantsUpdatedEventArgs) {
|
|
1334
|
+
onRosterChange(call)
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1237
1338
|
private final class AcsParticipantDelegateProxy: NSObject, RemoteParticipantDelegate {
|
|
1238
1339
|
private let onChanged: (RemoteParticipant) -> Void
|
|
1239
1340
|
|
|
@@ -1337,3 +1438,36 @@ private func parseAcsOutgoingVideo(_ raw: Any?) throws -> AcsOutgoingVideo {
|
|
|
1337
1438
|
}
|
|
1338
1439
|
return AcsOutgoingVideo(width: width, height: height, fps: fps, maxBitrateBps: bitrate)
|
|
1339
1440
|
}
|
|
1441
|
+
|
|
1442
|
+
/// Both identity modes use the same raw media and lifecycle code. Only agent creation and
|
|
1443
|
+
/// join options differ; the authenticated Teams display name is supplied by Microsoft.
|
|
1444
|
+
private func createMeetingAgent(client: CallClient, credential: CommunicationTokenCredential,
|
|
1445
|
+
guestOptions: CallAgentOptions, identityMode: String,
|
|
1446
|
+
completion: @escaping (CommonCallAgent?, Error?) -> Void)
|
|
1447
|
+
{
|
|
1448
|
+
switch identityMode {
|
|
1449
|
+
case "guest":
|
|
1450
|
+
client.createCallAgent(userCredential: credential, options: guestOptions) { completion($0, $1) }
|
|
1451
|
+
case "teams-user":
|
|
1452
|
+
client.createTeamsCallAgent(userCredential: credential, options: TeamsCallAgentOptions()) { completion($0, $1) }
|
|
1453
|
+
default:
|
|
1454
|
+
completion(nil, AcsMeetingError("Unsupported meeting identity mode"))
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
private func joinMeeting(agent: CommonCallAgent, locator: TeamsMeetingLinkLocator,
|
|
1459
|
+
guestOptions: JoinCallOptions,
|
|
1460
|
+
completion: @escaping (CommonCall?, Error?) -> Void)
|
|
1461
|
+
{
|
|
1462
|
+
if let teams = agent as? TeamsCallAgent {
|
|
1463
|
+
let options = JoinTeamsCallOptions()
|
|
1464
|
+
options.outgoingVideoOptions = guestOptions.outgoingVideoOptions
|
|
1465
|
+
options.outgoingAudioOptions = guestOptions.outgoingAudioOptions
|
|
1466
|
+
options.incomingAudioOptions = guestOptions.incomingAudioOptions
|
|
1467
|
+
teams.join(with: locator, joinTeamsCallOptions: options) { completion($0, $1) }
|
|
1468
|
+
} else if let guest = agent as? CallAgent {
|
|
1469
|
+
guest.join(with: locator, joinCallOptions: guestOptions) { completion($0, $1) }
|
|
1470
|
+
} else {
|
|
1471
|
+
completion(nil, AcsMeetingError("Unsupported meeting agent"))
|
|
1472
|
+
}
|
|
1473
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/acs-meeting",
|
|
3
|
-
"version": "3.2.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "MentraOS native ACS Teams meeting module (WHEP decode → ACS raw media)",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@expo/config-plugins": "~55.0.10",
|
|
35
|
-
"@mentra/glasses-media": "3.2.1
|
|
35
|
+
"@mentra/glasses-media": "3.2.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"expo-module-scripts": "^55.0.2",
|
package/src/AcsMeeting.types.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type AcsMeetingParticipant = {
|
|
|
23
23
|
export type AcsMeetingState = {
|
|
24
24
|
state: MeetingPhase
|
|
25
25
|
muted: boolean
|
|
26
|
+
/** Whether ACS is sending the glasses camera. Resets to true on every join. */
|
|
27
|
+
videoEnabled?: boolean
|
|
26
28
|
error?: string
|
|
27
29
|
meetingUrl?: string
|
|
28
30
|
provider?: "acs-teams"
|
|
@@ -51,6 +53,7 @@ export type AcsOutgoingVideo = {
|
|
|
51
53
|
export type AcsMeetingJoinOptions = {
|
|
52
54
|
meetingUrl: string
|
|
53
55
|
token: string
|
|
56
|
+
identityMode?: "guest" | "teams-user"
|
|
54
57
|
whepUrl?: string
|
|
55
58
|
videoSource?: {type: "whep"; url: string} | {type: "softap"; bindAddress: string}
|
|
56
59
|
displayName?: string
|
package/src/AcsMeetingModule.ts
CHANGED
|
@@ -3,9 +3,14 @@ import {NativeModule, requireNativeModule} from "expo"
|
|
|
3
3
|
import type {AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState} from "./AcsMeeting.types"
|
|
4
4
|
|
|
5
5
|
declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {
|
|
6
|
+
supportsTeamsIdentity(): boolean
|
|
6
7
|
join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>
|
|
7
8
|
/** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */
|
|
8
|
-
prepareAgent(options: {
|
|
9
|
+
prepareAgent(options: {
|
|
10
|
+
token: string
|
|
11
|
+
displayName?: string
|
|
12
|
+
identityMode?: "guest" | "teams-user"
|
|
13
|
+
}): Promise<AcsMeetingState>
|
|
9
14
|
leave(): Promise<void>
|
|
10
15
|
admitParticipant?(participantId: string): Promise<void>
|
|
11
16
|
/**
|
|
@@ -14,6 +19,11 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
14
19
|
*/
|
|
15
20
|
leaveAndAwait(options: {timeoutMs: number}): Promise<{completed: boolean}>
|
|
16
21
|
setMuted(muted: boolean): Promise<AcsMeetingState>
|
|
22
|
+
/**
|
|
23
|
+
* Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses
|
|
24
|
+
* media source and the preview tap stay up; frames are dropped before ACS while stopped.
|
|
25
|
+
*/
|
|
26
|
+
setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>
|
|
17
27
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>
|
|
18
28
|
updateVideoSource(whepUrl: string): Promise<void>
|
|
19
29
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
@@ -8,7 +8,11 @@ export default {
|
|
|
8
8
|
async join(_options: AcsMeetingJoinOptions): Promise<AcsMeetingState> {
|
|
9
9
|
unavailable()
|
|
10
10
|
},
|
|
11
|
-
async prepareAgent(_options: {
|
|
11
|
+
async prepareAgent(_options: {
|
|
12
|
+
token: string
|
|
13
|
+
displayName?: string
|
|
14
|
+
identityMode?: "guest" | "teams-user"
|
|
15
|
+
}): Promise<AcsMeetingState> {
|
|
12
16
|
unavailable()
|
|
13
17
|
},
|
|
14
18
|
async leave(): Promise<void> {
|
|
@@ -20,6 +24,9 @@ export default {
|
|
|
20
24
|
async setMuted(_muted: boolean): Promise<AcsMeetingState> {
|
|
21
25
|
unavailable()
|
|
22
26
|
},
|
|
27
|
+
async setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState> {
|
|
28
|
+
unavailable()
|
|
29
|
+
},
|
|
23
30
|
async setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState> {
|
|
24
31
|
unavailable()
|
|
25
32
|
},
|