@mentra/acs-meeting 3.2.0-dev.261 → 3.2.0-dev.262
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/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +30 -17
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +58 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/MediaDiagnosticsTest.kt +5 -0
- package/build/AcsMeeting.types.d.ts +1 -1
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +6 -0
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/build/AcsMeetingModule.web.d.ts +1 -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/AcsMeetingModule.swift +36 -0
- package/package.json +2 -2
- package/src/AcsMeeting.types.ts +1 -1
- package/src/AcsMeetingModule.ts +6 -0
- package/src/AcsMeetingModule.web.ts +3 -0
|
@@ -61,6 +61,28 @@ class AcsMeetingModule : Module() {
|
|
|
61
61
|
*/
|
|
62
62
|
private var internetHold: InternetHold? = null
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Lift the cellular process pin only across a SoftAP WHIP bind.
|
|
66
|
+
*
|
|
67
|
+
* A ServerSocket bound to 192.168.43.x while this UID is marked cellular accepts the bind
|
|
68
|
+
* but never sees the glasses' SYN — ICMP/ARP still work, TCP to the listener times out.
|
|
69
|
+
* Join already does this; recovery rebind must too.
|
|
70
|
+
*/
|
|
71
|
+
private inline fun <T> withIngestUnpinned(bind: () -> T): T {
|
|
72
|
+
val hold = internetHold
|
|
73
|
+
if (hold == null) {
|
|
74
|
+
SoftApTrace.stage("native_ingest_bind", "unpinned" to false, "reason" to "no cellular hold")
|
|
75
|
+
return bind()
|
|
76
|
+
}
|
|
77
|
+
hold.unbindProcess()
|
|
78
|
+
try {
|
|
79
|
+
SoftApTrace.stage("native_ingest_bind", "unpinned" to true)
|
|
80
|
+
return bind()
|
|
81
|
+
} finally {
|
|
82
|
+
hold.bindProcessToCellular()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
override fun definition() = ModuleDefinition {
|
|
65
87
|
Name("MentraAcsMeeting")
|
|
66
88
|
// `onScopedNetworkLost` fires only for a hotspot that went away while we still wanted it: the
|
|
@@ -303,23 +325,7 @@ class AcsMeetingModule : Module() {
|
|
|
303
325
|
video,
|
|
304
326
|
audioDelayMs,
|
|
305
327
|
origin,
|
|
306
|
-
bindIngestUnpinned = { bind ->
|
|
307
|
-
val hold = internetHold
|
|
308
|
-
if (hold == null) {
|
|
309
|
-
// No hold means no pin to lift, so the listener binds on whatever the default route
|
|
310
|
-
// is. Worth naming: that is also the state in which ACS's own sockets are unpinned.
|
|
311
|
-
SoftApTrace.stage("native_ingest_bind", "unpinned" to false, "reason" to "no cellular hold")
|
|
312
|
-
bind()
|
|
313
|
-
} else {
|
|
314
|
-
hold.unbindProcess()
|
|
315
|
-
try {
|
|
316
|
-
SoftApTrace.stage("native_ingest_bind", "unpinned" to true)
|
|
317
|
-
bind()
|
|
318
|
-
} finally {
|
|
319
|
-
hold.bindProcessToCellular()
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
},
|
|
328
|
+
bindIngestUnpinned = { bind -> withIngestUnpinned(bind) },
|
|
323
329
|
)
|
|
324
330
|
// Prefer the join snapshot: getState() can race a leave from a respawned miniapp
|
|
325
331
|
// restore and drop the URL the orchestrator needs to tell the glasses.
|
|
@@ -420,6 +426,13 @@ class AcsMeetingModule : Module() {
|
|
|
420
426
|
session?.restartVideoSource()
|
|
421
427
|
}
|
|
422
428
|
|
|
429
|
+
AsyncFunction("rebindSoftApIngest") {
|
|
430
|
+
traced("rebind_softap_ingest", "hasSession" to (session != null)) {
|
|
431
|
+
val meeting = session ?: throw IllegalStateException("No active meeting to rebind")
|
|
432
|
+
meeting.rebindSoftApIngest { bind -> withIngestUnpinned(bind) }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
423
436
|
AsyncFunction("getState") {
|
|
424
437
|
session?.getState() ?: mapOf("state" to "idle", "muted" to false)
|
|
425
438
|
}
|
|
@@ -747,6 +747,58 @@ class AcsMeetingSession(
|
|
|
747
747
|
*/
|
|
748
748
|
fun softApIngestUrl(): String? = media.ingestUrl
|
|
749
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Destroy the current SoftAP listener generation and bind a new one on the address the
|
|
752
|
+
* scoped network reports *now* — the caller rejoins first, then asks for this.
|
|
753
|
+
*
|
|
754
|
+
* Blocks on [executor] so it cannot race [join] / [leaveLocked]. Returns the new ingest URL
|
|
755
|
+
* the glasses must be told; throws rather than hand back a stale listener.
|
|
756
|
+
*/
|
|
757
|
+
fun rebindSoftApIngest(bindIngestUnpinned: (() -> Unit) -> Unit = { bind -> bind() }): String {
|
|
758
|
+
val done = CountDownLatch(1)
|
|
759
|
+
val result = AtomicReference<String?>(null)
|
|
760
|
+
val failure = AtomicReference<Exception?>(null)
|
|
761
|
+
val submittedAt = SystemClock.elapsedRealtime()
|
|
762
|
+
executor.execute {
|
|
763
|
+
try {
|
|
764
|
+
traceQueued("session_rebind_ingest_begin", submittedAt)
|
|
765
|
+
if (!MediaDiagnostics.SOFTAP_RECOVERY_ENABLED) {
|
|
766
|
+
throw IllegalStateException("SoftAP ingest rebind is disabled")
|
|
767
|
+
}
|
|
768
|
+
if (currentSourceKind != SourceKind.SOFTAP) {
|
|
769
|
+
throw IllegalStateException("rebindSoftApIngest is only valid for a SoftAP call")
|
|
770
|
+
}
|
|
771
|
+
val address = scopedNetwork?.localIpv4()
|
|
772
|
+
?: throw IllegalStateException("scoped network has no IPv4 address after rejoin")
|
|
773
|
+
// Same pin-lift as [join]: a listener bound while this UID is marked cellular
|
|
774
|
+
// never receives the glasses' TCP SYN, even though ping/ARP succeed.
|
|
775
|
+
lateinit var url: String
|
|
776
|
+
bindIngestUnpinned {
|
|
777
|
+
url = media.rebindIngest(
|
|
778
|
+
SourceConfig("", SourceKind.SOFTAP, address),
|
|
779
|
+
REBIND_INGEST_CLOSE_MS,
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
SoftApTrace.stage("session_rebind_ingest_end", "url" to url)
|
|
783
|
+
result.set(url)
|
|
784
|
+
} catch (error: Exception) {
|
|
785
|
+
SoftApTrace.failure(
|
|
786
|
+
"session_rebind_ingest_failed",
|
|
787
|
+
"reason" to "${error.javaClass.simpleName}: ${error.message ?: ""}",
|
|
788
|
+
)
|
|
789
|
+
failure.set(error)
|
|
790
|
+
} finally {
|
|
791
|
+
done.countDown()
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (!done.await(REBIND_INGEST_CLOSE_MS + 8_000L, TimeUnit.MILLISECONDS)) {
|
|
795
|
+
SoftApTrace.failure("session_rebind_ingest_timeout", "timeoutMs" to (REBIND_INGEST_CLOSE_MS + 8_000L))
|
|
796
|
+
throw IllegalStateException("SoftAP ingest rebind timed out")
|
|
797
|
+
}
|
|
798
|
+
failure.get()?.let { throw it }
|
|
799
|
+
return result.get() ?: throw IllegalStateException("SoftAP ingest rebind returned no URL")
|
|
800
|
+
}
|
|
801
|
+
|
|
750
802
|
/**
|
|
751
803
|
* Rebuild the WHEP subscription on the current URL even when it looks healthy.
|
|
752
804
|
* The host calls this when the phone changed networks: ICE may not have noticed
|
|
@@ -2001,6 +2053,12 @@ class AcsMeetingSession(
|
|
|
2001
2053
|
/** SoftAP join() blocks until the WHIP listener is bound and ACS join is queued. */
|
|
2002
2054
|
private const val SOFTAP_JOIN_WAIT_MS = 45_000L
|
|
2003
2055
|
|
|
2056
|
+
/**
|
|
2057
|
+
* How long [rebindSoftApIngest] waits for the parked listener to release its port after a
|
|
2058
|
+
* force-close. Not a tombstone: the old generation is closed immediately.
|
|
2059
|
+
*/
|
|
2060
|
+
private const val REBIND_INGEST_CLOSE_MS = 2_000L
|
|
2061
|
+
|
|
2004
2062
|
|
|
2005
2063
|
/**
|
|
2006
2064
|
* How long End waits for ACS to accept the hang-up before reporting it unconfirmed. Local
|
|
@@ -63,4 +63,9 @@ class MediaDiagnosticsTest {
|
|
|
63
63
|
fun timestampsDefaultOnAndRemainSwitchable() {
|
|
64
64
|
assertThat(MediaDiagnostics.acsAudioTimestamps).isTrue()
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
fun softapRecoveryShipsEnabled() {
|
|
69
|
+
assertThat(MediaDiagnostics.SOFTAP_RECOVERY_ENABLED).isTrue()
|
|
70
|
+
}
|
|
66
71
|
}
|
|
@@ -31,7 +31,7 @@ export type AcsMeetingState = {
|
|
|
31
31
|
/** ACS disconnect diagnostics, emitted when the final disconnected state arrives. */
|
|
32
32
|
endReason_code?: number;
|
|
33
33
|
endReason_subcode?: number;
|
|
34
|
-
/** Local WHIP endpoint, available after a SoftAP join
|
|
34
|
+
/** Local WHIP endpoint, available after a SoftAP join or `rebindSoftApIngest`. */
|
|
35
35
|
ingestUrl?: string;
|
|
36
36
|
/** Remote roster (Android emits this; iOS does not yet). */
|
|
37
37
|
participants?: AcsMeetingParticipant[];
|
|
@@ -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,
|
|
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,4DAA4D;IAC5D,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 +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
|
|
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 (Android emits this; iOS does not yet). */\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"]}
|
|
@@ -22,6 +22,12 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
22
22
|
updateVideoSource(whepUrl: string): Promise<void>;
|
|
23
23
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
24
24
|
restartVideoSource(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* SoftAP: destroy the current ingest listener generation and bind a new one.
|
|
27
|
+
* Resolves with the new URL the glasses must publish to. Rejects rather than
|
|
28
|
+
* reuse a stale listener.
|
|
29
|
+
*/
|
|
30
|
+
rebindSoftApIngest(): Promise<string>;
|
|
25
31
|
/** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */
|
|
26
32
|
joinScopedNetwork(ssid: string, passphrase: string): Promise<string>;
|
|
27
33
|
/** iOS: verify DHCP against the gateway advertised by the glasses before resolving. */
|
|
@@ -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;;;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,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,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;;;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;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AAsCtD,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 /**\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"]}
|
|
@@ -15,6 +15,7 @@ declare const _default: {
|
|
|
15
15
|
setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
16
16
|
updateVideoSource(_whepUrl: string): Promise<void>;
|
|
17
17
|
restartVideoSource(): Promise<void>;
|
|
18
|
+
rebindSoftApIngest(): Promise<string>;
|
|
18
19
|
joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string>;
|
|
19
20
|
leaveScopedNetwork(): Promise<void>;
|
|
20
21
|
probeScopedGateway(): Promise<{
|
|
@@ -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;
|
|
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 +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,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 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,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"]}
|
|
@@ -160,6 +160,19 @@ public class AcsMeetingModule: Module {
|
|
|
160
160
|
self.session?.restartVideoSource()
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
AsyncFunction("rebindSoftApIngest") { (promise: Promise) in
|
|
164
|
+
guard let session = self.session else {
|
|
165
|
+
promise.reject(AcsMeetingError("No active meeting to rebind"))
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
session.rebindSoftApIngest { result in
|
|
169
|
+
switch result {
|
|
170
|
+
case let .success(url): promise.resolve(url)
|
|
171
|
+
case let .failure(error): promise.reject(error)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
163
176
|
AsyncFunction("getState") {
|
|
164
177
|
self.session?.snapshot() ?? ["state": "idle", "muted": false]
|
|
165
178
|
}
|
|
@@ -236,6 +249,8 @@ final class AcsMeetingSession {
|
|
|
236
249
|
private var mediaRestartTask: DispatchWorkItem?
|
|
237
250
|
private static let mediaRestartBaseMs = 1000
|
|
238
251
|
private static let mediaRestartMaxMs = 10000
|
|
252
|
+
/// How long rebind waits for the parked listener to release after a force-close.
|
|
253
|
+
private static let rebindIngestCloseMs = 2000
|
|
239
254
|
private var joinGeneration: UInt64 = 0
|
|
240
255
|
private var capabilitiesFeature: CapabilitiesCallFeature?
|
|
241
256
|
/// nil means "not reported yet", which the miniapp shows as End disabled rather than absent.
|
|
@@ -574,6 +589,27 @@ final class AcsMeetingSession {
|
|
|
574
589
|
}
|
|
575
590
|
}
|
|
576
591
|
|
|
592
|
+
/// Destroy the current SoftAP listener generation and bind a new one on the
|
|
593
|
+
/// address the hotspot reports *now*. The caller rejoins first, then asks for this.
|
|
594
|
+
func rebindSoftApIngest(completion: @escaping (Result<String, Error>) -> Void) {
|
|
595
|
+
queue.async {
|
|
596
|
+
guard MediaDiagnostics.softapRecoveryEnabled else {
|
|
597
|
+
completion(.failure(AcsMeetingError("SoftAP ingest rebind is disabled")))
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
guard self.sourceConfig.kind == .softap, let source = self.media as? LocalWhipIngestSource else {
|
|
601
|
+
completion(.failure(AcsMeetingError("rebindSoftApIngest is only valid for a SoftAP call")))
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
guard let address = GlassesHotspotNetwork.wifiAddress() else {
|
|
605
|
+
completion(.failure(AcsMeetingError("scoped network has no IPv4 address after rejoin")))
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
let config = SourceConfig(url: "", kind: .softap, bindAddress: address)
|
|
609
|
+
source.rebindIngest(config: config, timeoutMs: Self.rebindIngestCloseMs, completion: completion)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
577
613
|
/// Rebuild the WHEP subscription on the current URL even when it looks healthy.
|
|
578
614
|
/// The host calls this when the phone changed networks.
|
|
579
615
|
func restartVideoSource() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/acs-meeting",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.262",
|
|
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.0-dev.
|
|
35
|
+
"@mentra/glasses-media": "3.2.0-dev.262"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"expo-module-scripts": "^55.0.2",
|
package/src/AcsMeeting.types.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type AcsMeetingState = {
|
|
|
35
35
|
/** ACS disconnect diagnostics, emitted when the final disconnected state arrives. */
|
|
36
36
|
endReason_code?: number
|
|
37
37
|
endReason_subcode?: number
|
|
38
|
-
/** Local WHIP endpoint, available after a SoftAP join
|
|
38
|
+
/** Local WHIP endpoint, available after a SoftAP join or `rebindSoftApIngest`. */
|
|
39
39
|
ingestUrl?: string
|
|
40
40
|
/** Remote roster (Android emits this; iOS does not yet). */
|
|
41
41
|
participants?: AcsMeetingParticipant[]
|
package/src/AcsMeetingModule.ts
CHANGED
|
@@ -17,6 +17,12 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
17
17
|
updateVideoSource(whepUrl: string): Promise<void>
|
|
18
18
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
19
19
|
restartVideoSource(): Promise<void>
|
|
20
|
+
/**
|
|
21
|
+
* SoftAP: destroy the current ingest listener generation and bind a new one.
|
|
22
|
+
* Resolves with the new URL the glasses must publish to. Rejects rather than
|
|
23
|
+
* reuse a stale listener.
|
|
24
|
+
*/
|
|
25
|
+
rebindSoftApIngest(): Promise<string>
|
|
20
26
|
/** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */
|
|
21
27
|
joinScopedNetwork(ssid: string, passphrase: string): Promise<string>
|
|
22
28
|
/** iOS: verify DHCP against the gateway advertised by the glasses before resolving. */
|
|
@@ -29,6 +29,9 @@ export default {
|
|
|
29
29
|
async restartVideoSource(): Promise<void> {
|
|
30
30
|
unavailable()
|
|
31
31
|
},
|
|
32
|
+
async rebindSoftApIngest(): Promise<string> {
|
|
33
|
+
unavailable()
|
|
34
|
+
},
|
|
32
35
|
async joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string> {
|
|
33
36
|
unavailable()
|
|
34
37
|
},
|