@mentra/acs-meeting 3.2.1-dev.311 → 3.2.1-dev.314

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.
@@ -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
@@ -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.Call
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
@@ -200,8 +204,8 @@ class AcsMeetingSession(
200
204
  private var noNetworkListener: DiagnosticFlagChangedListener? = null
201
205
  private var relaysListener: DiagnosticFlagChangedListener? = null
202
206
  private var callClient: CallClient? = null
203
- private var callAgent: CallAgent? = null
204
- private var call: Call? = null
207
+ private var callAgent: CommonCallAgent? = null
208
+ private var call: CommonCall? = null
205
209
  private var audioOut: RawOutgoingAudioStream? = null
206
210
  private var localOut: LocalOutgoingAudioStream? = null
207
211
  private var audioIn: RawIncomingAudioStream? = null
@@ -265,13 +269,15 @@ class AcsMeetingSession(
265
269
  * `createCallAgent` Future we stopped waiting on. ACS still finishes signing in; this is the only
266
270
  * handle that can dispose that leftover agent before the next join.
267
271
  */
268
- private var abandonedAgent: Future<CallAgent>? = null
272
+ private var abandonedAgent: Future<out CommonCallAgent>? = null
269
273
  /**
270
274
  * Agent signed in before the glasses hotspot came up. SoftAP DNS cannot resolve ACS hosts, so
271
275
  * `createCallAgent` after the scoped join stalls until the hotspot is torn down. Join reuses this
272
276
  * instead of signing in again on the broken resolver.
273
277
  */
274
278
  @Volatile private var agentPrepared = false
279
+ private var preparedToken: String? = null
280
+ private var preparedIdentityMode: String? = null
275
281
  private val controller = SessionAudioController()
276
282
  private val applier = AudioPolicyApplier(controller, scheduler) { Log.i(TAG, it) }
277
283
 
@@ -314,7 +320,7 @@ class AcsMeetingSession(
314
320
  * hotspot too. Here nothing is torn down while we wait, and a cold sign-in on a slow AP measured
315
321
  * 35 s on device — under the old 20 s cap that agent arrived just in time to be thrown away.
316
322
  */
317
- fun prepareAgent(token: String, displayName: String?) {
323
+ fun prepareAgent(token: String, displayName: String?, identityMode: String = "guest") {
318
324
  val done = CountDownLatch(1)
319
325
  val error = AtomicReference<Exception?>(null)
320
326
  phase = "connecting"
@@ -332,8 +338,10 @@ class AcsMeetingSession(
332
338
  callClient = CallClient()
333
339
  val agentOptions = CallAgentOptions()
334
340
  agentOptions.displayName = displayName ?: "Mentra Call"
335
- callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, PREPARE_AGENT_WAIT_MS)
341
+ callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, identityMode, PREPARE_AGENT_WAIT_MS)
336
342
  agentPrepared = true
343
+ preparedToken = token
344
+ preparedIdentityMode = identityMode
337
345
  Log.i(TAG, "ACS call agent prepared before SoftAP")
338
346
  SoftApTrace.stage(
339
347
  "session_prepare_agent_end",
@@ -379,6 +387,7 @@ class AcsMeetingSession(
379
387
  audioDelayMs: Int? = null,
380
388
  /** `created` (Start) or `joined` (Join); see [callOrigin]. Diagnostic only. */
381
389
  origin: String = "unknown",
390
+ identityMode: String = "guest",
382
391
  /**
383
392
  * Runs the WHIP listener bind, and exists so the caller can lift a process-wide network pin
384
393
  * across exactly that call. Takes the block rather than being a pair of before/after hooks so
@@ -426,7 +435,7 @@ class AcsMeetingSession(
426
435
  // the host and miniapp flash out of "joining" on every join.
427
436
  // A SoftAP join that already signed in on cellular must keep that agent:
428
437
  // recreating it on the glasses hotspot is the ACS_AGENT_TIMEOUT we just hit.
429
- val reuseAgent = agentPrepared && callAgent != null
438
+ val reuseAgent = agentPrepared && callAgent != null && preparedToken == token && preparedIdentityMode == identityMode
430
439
  leaveLocked(emitIdle = false, keepAgent = reuseAgent)
431
440
  // After the teardown, because that teardown bumps the generation itself. Everything that
432
441
  // moves it runs on this executor, so the value is stable for the rest of this join.
@@ -483,7 +492,7 @@ class AcsMeetingSession(
483
492
  callClient = CallClient()
484
493
  val agentOptions = CallAgentOptions()
485
494
  agentOptions.displayName = displayName ?: "Mentra Call"
486
- callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation)
495
+ callAgent = obtainCallAgent(callClient!!, credential, agentOptions, generation, identityMode)
487
496
  }
488
497
 
489
498
  val videoOptions = RawOutgoingVideoStreamOptions()
@@ -641,7 +650,12 @@ class AcsMeetingSession(
641
650
  }
642
651
 
643
652
  val locator = TeamsMeetingLinkLocator(teamsUrl)
644
- val joined = callAgent!!.join(context, locator, joinOptions)
653
+ val joined = when (val agent = callAgent!!) {
654
+ is TeamsCallAgent -> agent.join(context, locator, JoinTeamsCallOptions()
655
+ .setOutgoingVideoOptions(ov).setOutgoingAudioOptions(oa).setIncomingAudioOptions(ia))
656
+ is CallAgent -> agent.join(context, locator, joinOptions)
657
+ else -> throw IllegalStateException("Unsupported meeting agent")
658
+ }
645
659
  call = joined
646
660
  roster.attach(joined)
647
661
  joined.addOnStateChangedListener { pushCallState(joined.state) }
@@ -1224,7 +1238,7 @@ class AcsMeetingSession(
1224
1238
  ""
1225
1239
  }
1226
1240
 
1227
- private fun attachMediaStats(joined: Call) {
1241
+ private fun attachMediaStats(joined: CommonCall) {
1228
1242
  detachMediaStats()
1229
1243
  mediaStatsReports.set(0)
1230
1244
  lastWireSizeKey = null
@@ -1443,7 +1457,7 @@ class AcsMeetingSession(
1443
1457
  * "sender's video is frozen" guidance points at these diagnostics instead:
1444
1458
  * they are the only send-side network signal the SDK exposes.
1445
1459
  */
1446
- private fun attachDiagnostics(joined: Call) {
1460
+ private fun attachDiagnostics(joined: CommonCall) {
1447
1461
  detachDiagnostics()
1448
1462
  try {
1449
1463
  val feature = joined.feature(Features.LOCAL_USER_DIAGNOSTICS) as LocalUserDiagnosticsCallFeature
@@ -1638,7 +1652,7 @@ class AcsMeetingSession(
1638
1652
  * The capability that matters is `HANG_UP_FOR_EVERYONE`. It can flip mid-call — a presenter role
1639
1653
  * granted or removed — so the listener stays attached rather than reading once at connect.
1640
1654
  */
1641
- private fun attachCapabilities(joined: Call) {
1655
+ private fun attachCapabilities(joined: CommonCall) {
1642
1656
  detachCapabilities()
1643
1657
  try {
1644
1658
  val feature = joined.feature(Features.CAPABILITIES)
@@ -1750,16 +1764,17 @@ class AcsMeetingSession(
1750
1764
  credential: CommunicationTokenCredential,
1751
1765
  options: CallAgentOptions,
1752
1766
  generation: Int,
1767
+ identityMode: String,
1753
1768
  waitMs: Long = CALL_AGENT_WAIT_MS,
1754
- ): CallAgent {
1769
+ ): CommonCallAgent {
1755
1770
  disposeAbandonedAgent(waitMs = ABANDONED_AGENT_REJOIN_WAIT_MS)
1756
1771
  return try {
1757
- awaitCallAgent(client, credential, options, generation, waitMs)
1772
+ awaitCallAgent(client, credential, options, generation, identityMode, waitMs)
1758
1773
  } catch (error: Exception) {
1759
1774
  if (!AbandonedCallAgent.isExistingAgentError(error)) throw error
1760
1775
  Log.w(TAG, "createCallAgent hit leftover identity; disposing abandoned agent and retrying")
1761
1776
  disposeAbandonedAgent(waitMs = ABANDONED_AGENT_REJOIN_WAIT_MS)
1762
- awaitCallAgent(client, credential, options, generation, waitMs)
1777
+ awaitCallAgent(client, credential, options, generation, identityMode, waitMs)
1763
1778
  }
1764
1779
  }
1765
1780
 
@@ -1768,11 +1783,16 @@ class AcsMeetingSession(
1768
1783
  credential: CommunicationTokenCredential,
1769
1784
  options: CallAgentOptions,
1770
1785
  generation: Int,
1786
+ identityMode: String,
1771
1787
  waitMs: Long,
1772
- ): CallAgent {
1788
+ ): CommonCallAgent {
1773
1789
  val startedAt = SystemClock.elapsedRealtime()
1774
1790
  SoftApTrace.stage("session_call_agent_wait", "waitMs" to waitMs, "generation" to generation)
1775
- val pending = client.createCallAgent(context, credential, options)
1791
+ val pending: Future<out CommonCallAgent> = when (identityMode) {
1792
+ "guest" -> client.createCallAgent(context, credential, options)
1793
+ "teams-user" -> client.createTeamsCallAgent(context, credential, TeamsCallAgentOptions())
1794
+ else -> throw IllegalArgumentException("Unsupported meeting identity mode")
1795
+ }
1776
1796
  val agent = try {
1777
1797
  pending.get(waitMs, TimeUnit.MILLISECONDS)
1778
1798
  } catch (timeout: TimeoutException) {
@@ -1815,7 +1835,7 @@ class AcsMeetingSession(
1815
1835
  * Gives up after a bounded number of sweeps: by then the process has either got the agent or the
1816
1836
  * future is never completing, and an endless timer is its own leak.
1817
1837
  */
1818
- private fun sweepLateAgent(pending: Future<CallAgent>, sweep: Int) {
1838
+ private fun sweepLateAgent(pending: Future<out CommonCallAgent>, sweep: Int) {
1819
1839
  if (sweep >= LATE_AGENT_SWEEPS) {
1820
1840
  Log.w(TAG, "abandoned call agent never completed; stopping sweep")
1821
1841
  // Giving up on the sweep is giving up on disposing that agent. It is a bounded leak by
@@ -1992,6 +2012,8 @@ class AcsMeetingSession(
1992
2012
  callClient = null
1993
2013
  call = null
1994
2014
  callAgent = null
2015
+ preparedToken = null
2016
+ preparedIdentityMode = null
1995
2017
  agentPrepared = false
1996
2018
  }
1997
2019
  media.stop()
@@ -2151,7 +2173,7 @@ class AcsMeetingSession(
2151
2173
  null -> null
2152
2174
  }
2153
2175
 
2154
- private fun describeEndReason(call: Call?): Map<String, Any?> {
2176
+ private fun describeEndReason(call: CommonCall?): Map<String, Any?> {
2155
2177
  if (call == null) return mapOf("hasCall" to false)
2156
2178
  return try {
2157
2179
  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.Call
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: Call,
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.Call
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: Call? = null
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: Call) {
28
+ fun attach(joined: CommonCall) {
29
29
  detach()
30
30
  call = joined
31
31
  val listener = ParticipantsUpdatedListener { event ->
@@ -45,6 +45,7 @@ export type AcsOutgoingVideo = {
45
45
  export type AcsMeetingJoinOptions = {
46
46
  meetingUrl: string;
47
47
  token: string;
48
+ identityMode?: "guest" | "teams-user";
48
49
  whepUrl?: string;
49
50
  videoSource?: {
50
51
  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,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 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>;
@@ -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;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,eAAe,CAAC;IACtF,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,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
+ {"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,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;AAuCtD,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 join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>\n /** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */\n prepareAgent(options: {token: string; displayName?: string}): 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 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"]}
1
+ {"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AA4CtD,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 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: {
@@ -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;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,eAAe,CAAC;aAG9E,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;4BAG3B,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;;;;;;AAnC5C,wBA0CC"}
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;4BAG3B,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;;;;;;AAvC5C,wBA8CC"}
@@ -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,QAA+C;QAChE,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,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: {token: string; displayName?: string}): 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 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"]}
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,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 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"]}
@@ -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
@@ -239,12 +240,13 @@ final class AcsMeetingSession {
239
240
  private var lastError: String?
240
241
  private var callEndReason: (code: Int, subcode: Int)?
241
242
  private var callClient: CallClient?
242
- private var callAgent: CallAgent?
243
- private var call: Call?
243
+ private var callAgent: CommonCallAgent?
244
+ private var call: CommonCall?
244
245
  private var media: DecodedGlassesMediaSource?
245
246
  private var sourceConfig = SourceConfig(url: "")
246
- private var preparedAgent: CallAgent?
247
+ private var preparedAgent: CommonCallAgent?
247
248
  private var preparedClient: CallClient?
249
+ private var preparedIdentityMode: String?
248
250
  private var preparedToken: String?
249
251
  private var pendingJoin: ((Result<[String: Any], Error>) -> Void)?
250
252
  private var pendingPrepare: ((Result<[String: Any], Error>) -> Void)?
@@ -281,6 +283,11 @@ final class AcsMeetingSession {
281
283
  onMuteChange: { [weak self] call in self?.handleCallMuteChange(call) },
282
284
  onRosterChange: { [weak self] call in self?.refreshRoster(call) }
283
285
  )
286
+ private lazy var teamsCallDelegateProxy = AcsTeamsCallDelegateProxy(
287
+ onStateChange: { [weak self] call in self?.handleCallStateChange(call) },
288
+ onMuteChange: { [weak self] call in self?.handleCallMuteChange(call) },
289
+ onRosterChange: { [weak self] call in self?.refreshRoster(call) }
290
+ )
284
291
  private var remoteParticipants: [ObjectIdentifier: RemoteParticipant] = [:]
285
292
  private lazy var participantDelegateProxy = AcsParticipantDelegateProxy { [weak self] participant in
286
293
  self?.queue.async {
@@ -342,7 +349,7 @@ final class AcsMeetingSession {
342
349
  return result
343
350
  }
344
351
 
345
- func prepareAgent(token: String, displayName: String?, completion: @escaping (Result<[String: Any], Error>) -> Void) {
352
+ func prepareAgent(token: String, displayName: String?, identityMode: String = "guest", completion: @escaping (Result<[String: Any], Error>) -> Void) {
346
353
  queue.async {
347
354
  guard self.cleanup.wait(timeout: .now()) == .success, self.pendingPrepare == nil, self.pendingJoin == nil, self.call == nil, self.media == nil else {
348
355
  completion(.failure(AcsMeetingError("Previous call cleanup is still pending"))); return
@@ -356,7 +363,7 @@ final class AcsMeetingSession {
356
363
  let credential = try CommunicationTokenCredential(token: token)
357
364
  let options = CallAgentOptions()
358
365
  options.displayName = displayName ?? "Mentra Call"
359
- client.createCallAgent(userCredential: credential, options: options) { agent, error in
366
+ createMeetingAgent(client: client, credential: credential, guestOptions: options, identityMode: identityMode) { agent, error in
360
367
  self.queue.async {
361
368
  guard self.joinGeneration == generation, self.pendingPrepare != nil else { agent?.dispose(); return }
362
369
  let reply = self.pendingPrepare
@@ -364,6 +371,7 @@ final class AcsMeetingSession {
364
371
  if let agent, error == nil {
365
372
  self.preparedAgent = agent
366
373
  self.preparedToken = token
374
+ self.preparedIdentityMode = identityMode
367
375
  reply?(.success(self.snapshotLocked()))
368
376
  } else {
369
377
  agent?.dispose()
@@ -384,7 +392,7 @@ final class AcsMeetingSession {
384
392
  }
385
393
  }
386
394
 
387
- func join(token: String, meetingUrl: String, sourceConfig: SourceConfig, displayName: String?, dumpWav: Bool,
395
+ func join(token: String, identityMode: String = "guest", meetingUrl: String, sourceConfig: SourceConfig, displayName: String?, dumpWav: Bool,
388
396
  audioSource: String = "glasses", video: AcsOutgoingVideo = .hd,
389
397
  completion: @escaping (Result<[String: Any], Error>) -> Void)
390
398
  {
@@ -392,7 +400,7 @@ final class AcsMeetingSession {
392
400
  guard self.cleanup.wait(timeout: .now()) == .success, self.pendingPrepare == nil, self.pendingJoin == nil, self.call == nil, self.media == nil else {
393
401
  completion(.failure(AcsMeetingError("Previous call cleanup is still pending"))); return
394
402
  }
395
- let prepared = self.preparedToken == token ? self.preparedAgent : nil
403
+ let prepared = self.preparedToken == token && self.preparedIdentityMode == identityMode ? self.preparedAgent : nil
396
404
  let preparedClient = self.preparedClient
397
405
  if prepared != nil { self.preparedAgent = nil; self.preparedClient = nil; self.preparedToken = nil }
398
406
  self.leaveLocked(emitIdle: false)
@@ -404,7 +412,7 @@ final class AcsMeetingSession {
404
412
  self.lastError = nil
405
413
  self.callEndReason = nil
406
414
  self.emit("connecting")
407
- let useAgent: (CallAgent) -> Void = { agent in
415
+ let useAgent: (CommonCallAgent) -> Void = { agent in
408
416
  do { try self.joinWithAgentLocked(agent, generation: generation, meetingUrl: meetingUrl,
409
417
  sourceConfig: sourceConfig, dumpWav: dumpWav, video: video) } catch { self.failJoinLocked(error, generation: generation) }
410
418
  }
@@ -421,7 +429,7 @@ final class AcsMeetingSession {
421
429
  options.displayName = displayName ?? "Mentra Call"
422
430
  let agentStartedAt = ProcessInfo.processInfo.systemUptime
423
431
  NSLog("ACS-SPIKE iOS agent creation started generation=\(generation)")
424
- client.createCallAgent(userCredential: credential, options: options) { agent, error in
432
+ createMeetingAgent(client: client, credential: credential, guestOptions: options, identityMode: identityMode) { agent, error in
425
433
  self.queue.async {
426
434
  let elapsedMs = Int((ProcessInfo.processInfo.systemUptime - agentStartedAt) * 1000)
427
435
  NSLog("ACS-SPIKE iOS agent creation completed generation=\(generation) elapsedMs=\(elapsedMs) success=\(agent != nil && error == nil)")
@@ -444,7 +452,7 @@ final class AcsMeetingSession {
444
452
  }
445
453
 
446
454
  private func joinWithAgentLocked(
447
- _ agent: CallAgent,
455
+ _ agent: CommonCallAgent,
448
456
  generation: UInt64,
449
457
  meetingUrl: String,
450
458
  sourceConfig: SourceConfig,
@@ -523,14 +531,14 @@ final class AcsMeetingSession {
523
531
  let locator = TeamsMeetingLinkLocator(meetingLink: meetingUrl)
524
532
  // Cancellation reserves cleanup and retains the agent until the late join result
525
533
  // can be hung up. The retirement deadline still handles an SDK callback that is lost.
526
- let joinRetirement = CallJoinRetirement<Call>(group: cleanup, queue: queue, dispose: { agent.dispose() }) { call, finished in
534
+ let joinRetirement = CallJoinRetirement<CommonCall>(group: cleanup, queue: queue, dispose: { agent.dispose() }) { call, finished in
527
535
  call.hangUp(options: nil) { error in
528
536
  if let error { NSLog("ACS-SPIKE cancelled join hangUp failed: \(error)") }
529
537
  finished()
530
538
  }
531
539
  }
532
540
  cancelPendingCallJoin = { joinRetirement.cancel() }
533
- agent.join(with: locator, joinCallOptions: joinOptions) { call, error in
541
+ joinMeeting(agent: agent, locator: locator, guestOptions: joinOptions) { call, error in
534
542
  self.queue.async {
535
543
  guard joinRetirement.receive(call) else { return }
536
544
  guard self.callAgent === agent, self.joinGeneration == generation else {
@@ -559,7 +567,7 @@ final class AcsMeetingSession {
559
567
  }
560
568
 
561
569
  private func finishJoinLocked(
562
- _ call: Call,
570
+ _ call: CommonCall,
563
571
  generation: UInt64,
564
572
  sourceConfig: SourceConfig,
565
573
  dumpWav: Bool,
@@ -571,7 +579,8 @@ final class AcsMeetingSession {
571
579
  return
572
580
  }
573
581
  self.call = call
574
- call.delegate = callDelegateProxy
582
+ (call as? Call)?.delegate = callDelegateProxy
583
+ (call as? TeamsCall)?.delegate = teamsCallDelegateProxy
575
584
  refreshRosterLocked(call)
576
585
  attachCapabilities(call)
577
586
 
@@ -845,7 +854,7 @@ final class AcsMeetingSession {
845
854
  promoted to presenter mid-call), and an End button that never turns on is the same bug as one
846
855
  that lies about what it does.
847
856
  */
848
- private func attachCapabilities(_ call: Call) {
857
+ private func attachCapabilities(_ call: CommonCall) {
849
858
  let feature = call.feature(Features.capabilities)
850
859
  capabilitiesFeature = feature
851
860
  feature.delegate = capabilitiesDelegateProxy
@@ -976,7 +985,8 @@ final class AcsMeetingSession {
976
985
  let leavingAgent = callAgent
977
986
  let cancelJoin = cancelPendingCallJoin
978
987
  cancelPendingCallJoin = nil
979
- leavingCall?.delegate = nil
988
+ (leavingCall as? Call)?.delegate = nil
989
+ (leavingCall as? TeamsCall)?.delegate = nil
980
990
  if let leavingCall {
981
991
  // CallAgent.dispose releases all local SDK resources. The retirement deadline
982
992
  // also disposes the agent if hangUp loses its callback, before opening the barrier.
@@ -1017,7 +1027,7 @@ final class AcsMeetingSession {
1017
1027
  }
1018
1028
  }
1019
1029
 
1020
- fileprivate func currentCall() -> Call? {
1030
+ fileprivate func currentCall() -> CommonCall? {
1021
1031
  call
1022
1032
  }
1023
1033
 
@@ -1029,14 +1039,14 @@ final class AcsMeetingSession {
1029
1039
  phoneMic.setEnabled(enabled)
1030
1040
  }
1031
1041
 
1032
- private func handleCallStateChange(_ changedCall: Call) {
1042
+ private func handleCallStateChange(_ changedCall: CommonCall) {
1033
1043
  queue.async {
1034
1044
  guard self.call === changedCall, self.lastError == nil else { return }
1035
1045
  self.refreshCallStateLocked(changedCall)
1036
1046
  }
1037
1047
  }
1038
1048
 
1039
- private func refreshCallStateLocked(_ changedCall: Call) {
1049
+ private func refreshCallStateLocked(_ changedCall: CommonCall) {
1040
1050
  switch changedCall.state {
1041
1051
  case .connecting: emit("connecting")
1042
1052
  case .inLobby: emit("lobby")
@@ -1071,14 +1081,14 @@ final class AcsMeetingSession {
1071
1081
  }
1072
1082
  }
1073
1083
 
1074
- private func handleCallMuteChange(_ changedCall: Call) {
1084
+ private func handleCallMuteChange(_ changedCall: CommonCall) {
1075
1085
  queue.async {
1076
1086
  guard self.call === changedCall, self.lastError == nil else { return }
1077
1087
  self.applyAudioPolicyOnQueue("outgoing-audio-state")
1078
1088
  }
1079
1089
  }
1080
1090
 
1081
- private func refreshRoster(_ changedCall: Call) {
1091
+ private func refreshRoster(_ changedCall: CommonCall) {
1082
1092
  queue.async {
1083
1093
  guard self.call === changedCall else { return }
1084
1094
  self.refreshRosterLocked(changedCall)
@@ -1086,7 +1096,7 @@ final class AcsMeetingSession {
1086
1096
  }
1087
1097
  }
1088
1098
 
1089
- private func refreshRosterLocked(_ changedCall: Call) {
1099
+ private func refreshRosterLocked(_ changedCall: CommonCall) {
1090
1100
  dispatchPrecondition(condition: .onQueue(queue))
1091
1101
  let current = Dictionary(uniqueKeysWithValues: changedCall.remoteParticipants.map {
1092
1102
  (ObjectIdentifier($0), $0)
@@ -1209,12 +1219,12 @@ final class SessionAudioController: AudioStreamController {
1209
1219
  }
1210
1220
 
1211
1221
  private final class AcsCallDelegateProxy: NSObject, CallDelegate {
1212
- private let onStateChange: (Call) -> Void
1213
- private let onMuteChange: (Call) -> Void
1214
- private let onRosterChange: (Call) -> Void
1222
+ private let onStateChange: (CommonCall) -> Void
1223
+ private let onMuteChange: (CommonCall) -> Void
1224
+ private let onRosterChange: (CommonCall) -> Void
1215
1225
 
1216
- init(onStateChange: @escaping (Call) -> Void, onMuteChange: @escaping (Call) -> Void,
1217
- onRosterChange: @escaping (Call) -> Void)
1226
+ init(onStateChange: @escaping (CommonCall) -> Void, onMuteChange: @escaping (CommonCall) -> Void,
1227
+ onRosterChange: @escaping (CommonCall) -> Void)
1218
1228
  {
1219
1229
  self.onStateChange = onStateChange
1220
1230
  self.onMuteChange = onMuteChange
@@ -1234,6 +1244,32 @@ private final class AcsCallDelegateProxy: NSObject, CallDelegate {
1234
1244
  }
1235
1245
  }
1236
1246
 
1247
+ private final class AcsTeamsCallDelegateProxy: NSObject, TeamsCallDelegate {
1248
+ private let onStateChange: (CommonCall) -> Void
1249
+ private let onMuteChange: (CommonCall) -> Void
1250
+ private let onRosterChange: (CommonCall) -> Void
1251
+
1252
+ init(onStateChange: @escaping (CommonCall) -> Void, onMuteChange: @escaping (CommonCall) -> Void,
1253
+ onRosterChange: @escaping (CommonCall) -> Void)
1254
+ {
1255
+ self.onStateChange = onStateChange
1256
+ self.onMuteChange = onMuteChange
1257
+ self.onRosterChange = onRosterChange
1258
+ }
1259
+
1260
+ func teamsCall(_ call: TeamsCall, didChangeState _: PropertyChangedEventArgs) {
1261
+ onStateChange(call)
1262
+ }
1263
+
1264
+ func teamsCall(_ call: TeamsCall, didUpdateOutgoingAudioState _: PropertyChangedEventArgs) {
1265
+ onMuteChange(call)
1266
+ }
1267
+
1268
+ func teamsCall(_ call: TeamsCall, didUpdateRemoteParticipant _: ParticipantsUpdatedEventArgs) {
1269
+ onRosterChange(call)
1270
+ }
1271
+ }
1272
+
1237
1273
  private final class AcsParticipantDelegateProxy: NSObject, RemoteParticipantDelegate {
1238
1274
  private let onChanged: (RemoteParticipant) -> Void
1239
1275
 
@@ -1337,3 +1373,36 @@ private func parseAcsOutgoingVideo(_ raw: Any?) throws -> AcsOutgoingVideo {
1337
1373
  }
1338
1374
  return AcsOutgoingVideo(width: width, height: height, fps: fps, maxBitrateBps: bitrate)
1339
1375
  }
1376
+
1377
+ /// Both identity modes use the same raw media and lifecycle code. Only agent creation and
1378
+ /// join options differ; the authenticated Teams display name is supplied by Microsoft.
1379
+ private func createMeetingAgent(client: CallClient, credential: CommunicationTokenCredential,
1380
+ guestOptions: CallAgentOptions, identityMode: String,
1381
+ completion: @escaping (CommonCallAgent?, Error?) -> Void)
1382
+ {
1383
+ switch identityMode {
1384
+ case "guest":
1385
+ client.createCallAgent(userCredential: credential, options: guestOptions) { completion($0, $1) }
1386
+ case "teams-user":
1387
+ client.createTeamsCallAgent(userCredential: credential, options: TeamsCallAgentOptions()) { completion($0, $1) }
1388
+ default:
1389
+ completion(nil, AcsMeetingError("Unsupported meeting identity mode"))
1390
+ }
1391
+ }
1392
+
1393
+ private func joinMeeting(agent: CommonCallAgent, locator: TeamsMeetingLinkLocator,
1394
+ guestOptions: JoinCallOptions,
1395
+ completion: @escaping (CommonCall?, Error?) -> Void)
1396
+ {
1397
+ if let teams = agent as? TeamsCallAgent {
1398
+ let options = JoinTeamsCallOptions()
1399
+ options.outgoingVideoOptions = guestOptions.outgoingVideoOptions
1400
+ options.outgoingAudioOptions = guestOptions.outgoingAudioOptions
1401
+ options.incomingAudioOptions = guestOptions.incomingAudioOptions
1402
+ teams.join(with: locator, joinTeamsCallOptions: options) { completion($0, $1) }
1403
+ } else if let guest = agent as? CallAgent {
1404
+ guest.join(with: locator, joinCallOptions: guestOptions) { completion($0, $1) }
1405
+ } else {
1406
+ completion(nil, AcsMeetingError("Unsupported meeting agent"))
1407
+ }
1408
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/acs-meeting",
3
- "version": "3.2.1-dev.311",
3
+ "version": "3.2.1-dev.314",
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-dev.311"
35
+ "@mentra/glasses-media": "3.2.1-dev.314"
36
36
  },
37
37
  "devDependencies": {
38
38
  "expo-module-scripts": "^55.0.2",
@@ -51,6 +51,7 @@ export type AcsOutgoingVideo = {
51
51
  export type AcsMeetingJoinOptions = {
52
52
  meetingUrl: string
53
53
  token: string
54
+ identityMode?: "guest" | "teams-user"
54
55
  whepUrl?: string
55
56
  videoSource?: {type: "whep"; url: string} | {type: "softap"; bindAddress: string}
56
57
  displayName?: string
@@ -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: {token: string; displayName?: string}): Promise<AcsMeetingState>
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
  /**
@@ -8,7 +8,11 @@ export default {
8
8
  async join(_options: AcsMeetingJoinOptions): Promise<AcsMeetingState> {
9
9
  unavailable()
10
10
  },
11
- async prepareAgent(_options: {token: string; displayName?: string}): Promise<AcsMeetingState> {
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> {