@mentra/acs-meeting 3.3.0-dev.328 → 3.3.0-dev.329
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +12 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +51 -2
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +9 -7
- package/build/AcsMeeting.types.d.ts +2 -0
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +5 -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/AcsFrameSender.swift +7 -4
- package/ios/AcsMeetingModule.swift +66 -1
- package/package.json +2 -2
- package/src/AcsMeeting.types.ts +2 -0
- package/src/AcsMeetingModule.ts +5 -0
- package/src/AcsMeetingModule.web.ts +3 -0
package/README.md
CHANGED
|
@@ -237,6 +237,7 @@ Audio and video control:
|
|
|
237
237
|
| Function | Notes |
|
|
238
238
|
|---|---|
|
|
239
239
|
| `setMuted(muted)` | Gates the uplink chain synchronously, then applies ACS policy |
|
|
240
|
+
| `setVideoEnabled(enabled)` | ACS `stopVideo`/`startVideo` on the virtual outgoing stream. Glasses source and preview tap keep running; `videoEnabled` resets to true per join |
|
|
240
241
|
| `setAudioSource(source)` | **No-op.** The source is locked for the call at join |
|
|
241
242
|
| `pushOutgoingPcm(base64, rate, ch)` | Synchronous `Function`, ~100 calls/s. Returns whether it entered the uplink |
|
|
242
243
|
| `updateVideoSource(whepUrl)` | WHEP only |
|
|
@@ -429,6 +429,18 @@ class AcsMeetingModule : Module() {
|
|
|
429
429
|
session?.setMuted(muted) ?: mapOf("state" to "idle", "muted" to muted)
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
AsyncFunction("setVideoEnabled") { enabled: Boolean, promise: Promise ->
|
|
433
|
+
val meeting = session
|
|
434
|
+
if (meeting == null) {
|
|
435
|
+
promise.reject("VIDEO_TOGGLE_FAILED", "No active meeting", null)
|
|
436
|
+
} else {
|
|
437
|
+
meeting.setVideoEnabled(enabled) { state, error ->
|
|
438
|
+
if (error != null) promise.reject("VIDEO_TOGGLE_FAILED", error.message, error)
|
|
439
|
+
else promise.resolve(state)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
432
444
|
AsyncFunction("setAudioSource") { source: String ->
|
|
433
445
|
session?.setAudioSource(source) ?: mapOf("state" to "idle", "muted" to false, "audioSource" to source)
|
|
434
446
|
}
|
|
@@ -74,6 +74,7 @@ import com.mentra.glassesmedia.source.OutgoingRateArm
|
|
|
74
74
|
import com.mentra.glassesmedia.source.PixelFormatArm
|
|
75
75
|
import com.mentra.glassesmedia.source.GlassesMediaController
|
|
76
76
|
import com.mentra.glassesmedia.network.ScopedSoftApNetwork
|
|
77
|
+
import com.mentra.glassesmedia.source.DecodedFrameTap
|
|
77
78
|
import com.mentra.glassesmedia.source.GlassesMediaSourceFactory
|
|
78
79
|
import com.mentra.glassesmedia.source.LocalWhipIngestSource
|
|
79
80
|
import com.mentra.acsmeeting.source.MeetingVideoSourceSpec
|
|
@@ -210,6 +211,8 @@ class AcsMeetingSession(
|
|
|
210
211
|
private var localOut: LocalOutgoingAudioStream? = null
|
|
211
212
|
private var audioIn: RawIncomingAudioStream? = null
|
|
212
213
|
private var videoOut: VirtualOutgoingVideoStream? = null
|
|
214
|
+
/** Whether ACS is sending [videoOut]. Only [setVideoEnabled] turns it off; every call starts on. */
|
|
215
|
+
@Volatile private var videoEnabled = true
|
|
213
216
|
@Volatile private var meetingUrl: String? = null
|
|
214
217
|
@Volatile private var phase = "idle"
|
|
215
218
|
@Volatile private var lastError: String? = null
|
|
@@ -285,6 +288,7 @@ class AcsMeetingSession(
|
|
|
285
288
|
val result = mutableMapOf<String, Any>(
|
|
286
289
|
"state" to phase,
|
|
287
290
|
"muted" to muted.get(),
|
|
291
|
+
"videoEnabled" to videoEnabled,
|
|
288
292
|
"provider" to "acs-teams",
|
|
289
293
|
"audioSource" to audioSource,
|
|
290
294
|
"activeStream" to controller.readActive().name.lowercase(),
|
|
@@ -641,7 +645,10 @@ class AcsMeetingSession(
|
|
|
641
645
|
bindIngestUnpinned {
|
|
642
646
|
media.attach(
|
|
643
647
|
video = { planes ->
|
|
644
|
-
|
|
648
|
+
// Preview first, deliberately: the tap must see frames ACS's pacing and
|
|
649
|
+
// readiness gates would otherwise hide, and it cannot delay or break this call.
|
|
650
|
+
DecodedFrameTap.offer(planes)
|
|
651
|
+
if (frameSender.sendPlanes(planes)) DecodedFrameTap.recordAcsSend()
|
|
645
652
|
},
|
|
646
653
|
pcm = { pcm, rate, channels -> feedOutgoingPcm(pcm, rate, channels) },
|
|
647
654
|
config = videoSource.toConfig(),
|
|
@@ -668,7 +675,8 @@ class AcsMeetingSession(
|
|
|
668
675
|
if (videoSource !is MeetingVideoSourceSpec.SoftAp) {
|
|
669
676
|
media.attach(
|
|
670
677
|
video = { planes ->
|
|
671
|
-
|
|
678
|
+
DecodedFrameTap.offer(planes)
|
|
679
|
+
if (frameSender.sendPlanes(planes)) DecodedFrameTap.recordAcsSend()
|
|
672
680
|
},
|
|
673
681
|
pcm = { pcm, rate, channels -> feedOutgoingPcm(pcm, rate, channels) },
|
|
674
682
|
config = if (synthetic) SourceConfig("", SourceKind.DIRECT) else videoSource.toConfig(),
|
|
@@ -900,6 +908,45 @@ class AcsMeetingSession(
|
|
|
900
908
|
return snap
|
|
901
909
|
}
|
|
902
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Stop or resume the camera Teams receives without leaving the call. The glasses source, the
|
|
913
|
+
* preview tap and the WHEP/WHIP transport keep running; [frameSender] drops frames while ACS
|
|
914
|
+
* reports the stream stopped. [videoEnabled] only moves once ACS accepts the change.
|
|
915
|
+
*/
|
|
916
|
+
fun setVideoEnabled(next: Boolean, complete: (Map<String, Any>?, Throwable?) -> Unit) {
|
|
917
|
+
val generation = joinGeneration.get()
|
|
918
|
+
executor.execute {
|
|
919
|
+
val active = call
|
|
920
|
+
val stream = videoOut
|
|
921
|
+
if (active == null || stream == null || joinGeneration.get() != generation) {
|
|
922
|
+
complete(null, IllegalStateException("No active meeting"))
|
|
923
|
+
return@execute
|
|
924
|
+
}
|
|
925
|
+
if (videoEnabled == next) {
|
|
926
|
+
complete(snapshot(), null)
|
|
927
|
+
return@execute
|
|
928
|
+
}
|
|
929
|
+
val result = runCatching {
|
|
930
|
+
val change = if (next) active.startVideo(context, stream) else active.stopVideo(context, stream)
|
|
931
|
+
change.get(VIDEO_TOGGLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
932
|
+
}
|
|
933
|
+
if (call !== active || joinGeneration.get() != generation) {
|
|
934
|
+
complete(null, IllegalStateException("The meeting ended before the camera changed"))
|
|
935
|
+
return@execute
|
|
936
|
+
}
|
|
937
|
+
result.exceptionOrNull()?.let { error ->
|
|
938
|
+
Log.e(TAG, "setVideoEnabled=$next failed", error)
|
|
939
|
+
complete(null, error)
|
|
940
|
+
return@execute
|
|
941
|
+
}
|
|
942
|
+
Log.i(TAG, "setVideoEnabled=$next")
|
|
943
|
+
videoEnabled = next
|
|
944
|
+
val snap = snapshot()
|
|
945
|
+
onState(snap)
|
|
946
|
+
complete(snap, null)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
903
950
|
fun setAudioSource(source: String): Map<String, Any> {
|
|
904
951
|
val parsed = AcsAudioPolicy.parseSource(source)
|
|
905
952
|
if (parsed == null) {
|
|
@@ -2021,6 +2068,7 @@ class AcsMeetingSession(
|
|
|
2021
2068
|
localOut = null
|
|
2022
2069
|
audioIn = null
|
|
2023
2070
|
videoOut = null
|
|
2071
|
+
videoEnabled = true
|
|
2024
2072
|
outgoingReady.set(false)
|
|
2025
2073
|
muted.set(false)
|
|
2026
2074
|
hangUpForEveryone = CapabilityStatus()
|
|
@@ -2097,6 +2145,7 @@ class AcsMeetingSession(
|
|
|
2097
2145
|
private const val TAG = "ACS-SPIKE"
|
|
2098
2146
|
const val GLASSES_REQUIRES_UNMUTED_TRANSPORT = true
|
|
2099
2147
|
private const val ROSTER_COALESCE_MS = 150L
|
|
2148
|
+
private const val VIDEO_TOGGLE_TIMEOUT_MS = 10_000L
|
|
2100
2149
|
private const val MEDIA_RESTART_BASE_MS = 1_000L
|
|
2101
2150
|
private const val MEDIA_RESTART_MAX_MS = 10_000L
|
|
2102
2151
|
|
|
@@ -103,20 +103,21 @@ class AcsFrameSender(
|
|
|
103
103
|
|
|
104
104
|
fun isReady(): Boolean = stream.get() != null && running.get() && format.get() != null
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/** True when the frame was submitted to ACS; false when a readiness, size, pacing or busy gate dropped it. */
|
|
107
|
+
fun sendPlanes(planes: I420Planes): Boolean {
|
|
107
108
|
val out = stream.get()
|
|
108
109
|
if (out == null || !running.get()) {
|
|
109
110
|
stats.onDropNotStarted()
|
|
110
|
-
return
|
|
111
|
+
return false
|
|
111
112
|
}
|
|
112
113
|
val negotiated = format.get()
|
|
113
114
|
if (negotiated == null) {
|
|
114
115
|
stats.onDropNotStarted()
|
|
115
|
-
return
|
|
116
|
+
return false
|
|
116
117
|
}
|
|
117
118
|
if (negotiated.width != planes.width || negotiated.height != planes.height) {
|
|
118
119
|
stats.onDropSize()
|
|
119
|
-
return
|
|
120
|
+
return false
|
|
120
121
|
}
|
|
121
122
|
if (!planes.planesReadable()) {
|
|
122
123
|
stats.onDropMalformed()
|
|
@@ -125,11 +126,11 @@ class AcsFrameSender(
|
|
|
125
126
|
"P5 send plane too small ${planes.width}x${planes.height} " +
|
|
126
127
|
"stride=${planes.strideY}/${planes.strideU}/${planes.strideV}",
|
|
127
128
|
)
|
|
128
|
-
return
|
|
129
|
+
return false
|
|
129
130
|
}
|
|
130
131
|
if (!pacer.tryAdmit(FramePacer.intervalNs(negotiated.framesPerSecond))) {
|
|
131
132
|
stats.onDropPaced()
|
|
132
|
-
return
|
|
133
|
+
return false
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
val prepared = prepareSend(planes, negotiated)
|
|
@@ -139,7 +140,7 @@ class AcsFrameSender(
|
|
|
139
140
|
if (!gate.tryAcquire()) {
|
|
140
141
|
stats.onDropBusy()
|
|
141
142
|
releaseSendBuffers(prepared, planes)
|
|
142
|
-
return
|
|
143
|
+
return false
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
val seq = sendSeq.incrementAndGet()
|
|
@@ -230,6 +231,7 @@ class AcsFrameSender(
|
|
|
230
231
|
gate.release()
|
|
231
232
|
releaseSendBuffers(prepared, planes)
|
|
232
233
|
}
|
|
234
|
+
return submitted
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
private data class PreparedSend(
|
|
@@ -19,6 +19,8 @@ export type AcsMeetingParticipant = {
|
|
|
19
19
|
export type AcsMeetingState = {
|
|
20
20
|
state: MeetingPhase;
|
|
21
21
|
muted: boolean;
|
|
22
|
+
/** Whether ACS is sending the glasses camera. Resets to true on every join. */
|
|
23
|
+
videoEnabled?: boolean;
|
|
22
24
|
error?: string;
|
|
23
25
|
meetingUrl?: string;
|
|
24
26
|
provider?: "acs-teams";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeeting.types.d.ts","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AcsMeeting.types.d.ts","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 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
|
+
{"version":3,"file":"AcsMeeting.types.js","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"","sourcesContent":["export type MeetingPhase = \"idle\" | \"connecting\" | \"lobby\" | \"connected\" | \"disconnected\" | \"error\"\n\nexport type AcsAudioSource = \"glasses\" | \"phone\"\nexport type AcsActiveStream = \"none\" | \"virtual\" | \"local\"\nexport type AcsAudioSafety = \"safe\" | \"degraded\" | \"unsafe\"\n/**\n * Health of the glasses WHEP subscription feeding the call. `failed` = ICE dropped or\n * the WHEP endpoint went away while the ACS call may still be `connected`. Native\n * rebuilds it on its own with backoff; the host can force one via `restartVideoSource`.\n */\nexport type AcsMediaSourceState = \"idle\" | \"connecting\" | \"live\" | \"failed\"\n\nexport type AcsMeetingParticipantState = \"idle\" | \"connecting\" | \"connected\" | \"lobby\" | \"hold\" | \"disconnected\"\n\nexport type AcsMeetingParticipant = {\n id: string\n displayName: string | null\n state: AcsMeetingParticipantState\n isMuted: boolean\n isSpeaking: boolean\n}\n\nexport type AcsMeetingState = {\n state: MeetingPhase\n muted: boolean\n /** Whether ACS is sending the glasses camera. Resets to true on every join. */\n videoEnabled?: boolean\n error?: string\n meetingUrl?: string\n provider?: \"acs-teams\"\n audioSource?: AcsAudioSource\n activeStream?: AcsActiveStream\n audioSafety?: AcsAudioSafety\n mediaSource?: AcsMediaSourceState\n /** Native receiver diagnostic, including the reason a local WHIP offer was rejected. */\n mediaSourceReason?: string\n /** ACS disconnect diagnostics, emitted when the final disconnected state arrives. */\n endReason_code?: number\n endReason_subcode?: number\n /** Local WHIP endpoint, available after a SoftAP join or `rebindSoftApIngest`. */\n ingestUrl?: string\n /** Remote roster, including participant state, mute state and speaking updates. */\n participants?: AcsMeetingParticipant[]\n}\n\nexport type AcsOutgoingVideo = {\n width: number\n height: number\n fps: number\n maxBitrateBps: number\n}\n\nexport type AcsMeetingJoinOptions = {\n meetingUrl: string\n token: string\n identityMode?: \"guest\" | \"teams-user\"\n whepUrl?: string\n videoSource?: {type: \"whep\"; url: string} | {type: \"softap\"; bindAddress: string}\n displayName?: string\n /** \"glasses\" sends WHEP PCM. \"phone\" uses the ACS local mic (handset or BT). */\n audioSource?: AcsAudioSource\n /**\n * Hold outgoing PCM this many ms before ACS. SoftAP+LC3 sets this in the host\n * because BLE audio reaches the phone before SoftAP video.\n */\n audioDelayMs?: number\n /** Dump WHEP PCM to a WAV in cache for P4 verification. */\n dumpPcmWav?: boolean\n /** When omitted, native keeps 1280×720@15 / 2.5 Mbps. */\n video?: AcsOutgoingVideo\n}\n\nexport type AcsIncomingPcmEvent = {\n base64: string\n sampleRate: number\n channels: number\n}\n\nexport type AcsMeetingModuleEvents = {\n onState: (state: AcsMeetingState) => void\n onIncomingPcm: (pcm: AcsIncomingPcmEvent) => void\n}\n"]}
|
|
@@ -21,6 +21,11 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
21
21
|
completed: boolean;
|
|
22
22
|
}>;
|
|
23
23
|
setMuted(muted: boolean): Promise<AcsMeetingState>;
|
|
24
|
+
/**
|
|
25
|
+
* Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses
|
|
26
|
+
* media source and the preview tap stay up; frames are dropped before ACS while stopped.
|
|
27
|
+
*/
|
|
28
|
+
setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>;
|
|
24
29
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
25
30
|
updateVideoSource(whepUrl: string): Promise<void>;
|
|
26
31
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,KAAK,EAAC,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;AAEtG,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IAC/E,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
|
+
{"version":3,"file":"AcsMeetingModule.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,KAAK,EAAC,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;AAEtG,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IAC/E,qBAAqB,IAAI,OAAO;IAChC,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAC9D,iFAAiF;IACjF,YAAY,CAAC,OAAO,EAAE;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;KACtC,GAAG,OAAO,CAAC,eAAe,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,gBAAgB,CAAC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACvD;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAClD;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAC3D,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IACrE,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACjD,wEAAwE;IACxE,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IACnC;;;;OAIG;IACH,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;IACrC,4EAA4E;IAC5E,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACpE,uFAAuF;IACvF,4BAA4B,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACjG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1C,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IACnC,uBAAuB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IACzC,+BAA+B,CAAC,IAAI,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,CAAC;IACjG,qEAAqE;IACrE,kBAAkB,IAAI,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;IACnE,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC;CACrC;;AAED,wBAA8E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.js","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AAiDtD,eAAe,mBAAmB,CAAyB,kBAAkB,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport type {AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState} from \"./AcsMeeting.types\"\n\ndeclare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {\n supportsTeamsIdentity(): boolean\n join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>\n /** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */\n prepareAgent(options: {\n token: string\n displayName?: string\n identityMode?: \"guest\" | \"teams-user\"\n }): Promise<AcsMeetingState>\n leave(): Promise<void>\n admitParticipant?(participantId: string): Promise<void>\n /**\n * Leave, and resolve only once the hang-up, the agent disposal, and the network releases have\n * finished. Use this explicit barrier across platforms; Android `leave()` only queues cleanup.\n */\n leaveAndAwait(options: {timeoutMs: number}): Promise<{completed: boolean}>\n setMuted(muted: boolean): Promise<AcsMeetingState>\n /**\n * Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses\n * media source and the preview tap stay up; frames are dropped before ACS while stopped.\n */\n setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>\n setAudioSource(source: \"glasses\" | \"phone\"): Promise<AcsMeetingState>\n updateVideoSource(whepUrl: string): Promise<void>\n /** Force a WHEP rebuild on the current URL (phone changed networks). */\n restartVideoSource(): Promise<void>\n /**\n * SoftAP: destroy the current ingest listener generation and bind a new one.\n * Resolves with the new URL the glasses must publish to. Rejects rather than\n * reuse a stale listener.\n */\n rebindSoftApIngest(): Promise<string>\n /** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */\n joinScopedNetwork(ssid: string, passphrase: string): Promise<string>\n /** iOS: verify DHCP against the gateway advertised by the glasses before resolving. */\n joinScopedNetworkWithGateway?(ssid: string, passphrase: string, gateway: string): Promise<string>\n beginTrace(traceId: string): Promise<void>\n leaveScopedNetwork(): Promise<void>\n cancelScopedNetworkJoin?(): Promise<void>\n awaitDefaultNetworkAfterHotspot?(): Promise<{usable: boolean; detail: string; transport: string}>\n /** SoftAP: TCP-probe the hotspot gateway over the scoped network. */\n probeScopedGateway(): Promise<{reachable: boolean; detail: string}>\n getState(): Promise<AcsMeetingState>\n}\n\nexport default requireNativeModule<AcsMeetingNativeModule>(\"MentraAcsMeeting\")\n"]}
|
|
@@ -13,6 +13,7 @@ declare const _default: {
|
|
|
13
13
|
completed: boolean;
|
|
14
14
|
}>;
|
|
15
15
|
setMuted(_muted: boolean): Promise<AcsMeetingState>;
|
|
16
|
+
setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState>;
|
|
16
17
|
setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
17
18
|
updateVideoSource(_whepUrl: string): Promise<void>;
|
|
18
19
|
restartVideoSource(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.web.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;;mBAOvD,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;2BAGxC;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;
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.web.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;;mBAOvD,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;2BAGxC;QAC3B,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;KACtC,GAAG,OAAO,CAAC,eAAe,CAAC;aAGb,OAAO,CAAC,IAAI,CAAC;4BAGE;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;qBAG1D,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;8BAGzB,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;4BAGpC,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;gCAG1C,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;0BAG5B,OAAO,CAAC,IAAI,CAAC;0BAGb,OAAO,CAAC,MAAM,CAAC;6BAGZ,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;0BAGhD,OAAO,CAAC,IAAI,CAAC;0BACb,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;gBAGvD,OAAO,CAAC,eAAe,CAAC;;;;;;AA1C5C,wBAiDC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AcsMeetingModule.web.js","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAEA,SAAS,WAAW;IAClB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;AACxD,CAAC;AAED,eAAe;IACb,KAAK,CAAC,IAAI,CAAC,QAA+B;QACxC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,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"]}
|
|
1
|
+
{"version":3,"file":"AcsMeetingModule.web.js","sourceRoot":"","sources":["../src/AcsMeetingModule.web.ts"],"names":[],"mappings":"AAEA,SAAS,WAAW;IAClB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;AACxD,CAAC;AAED,eAAe;IACb,KAAK,CAAC,IAAI,CAAC,QAA+B;QACxC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,QAIlB;QACC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,KAAK;QACT,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,QAA6B;QAC/C,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,MAAe;QAC5B,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,eAAe,CAAC,QAAiB;QACrC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,OAA4B;QAC/C,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,QAAgB;QACtC,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB;QACtB,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB;QACtB,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,KAAa,EAAE,WAAmB;QACxD,WAAW,EAAE,CAAA;IACf,CAAC;IACD,KAAK,CAAC,kBAAkB,KAAmB,CAAC;IAC5C,KAAK,CAAC,kBAAkB;QACtB,OAAO,EAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,EAAC,CAAA;IAC/D,CAAC;IACD,KAAK,CAAC,QAAQ;QACZ,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;IACtC,CAAC;IACD,WAAW;QACT,OAAO,EAAC,MAAM,KAAI,CAAC,EAAC,CAAA;IACtB,CAAC;IACD,eAAe,KAAI,CAAC;CACrB,CAAA","sourcesContent":["import type {AcsMeetingJoinOptions, AcsMeetingState} from \"./AcsMeeting.types\"\n\nfunction unavailable(): never {\n throw new Error(\"ACS meeting is not available on web\")\n}\n\nexport default {\n async join(_options: AcsMeetingJoinOptions): Promise<AcsMeetingState> {\n unavailable()\n },\n async prepareAgent(_options: {\n token: string\n displayName?: string\n identityMode?: \"guest\" | \"teams-user\"\n }): Promise<AcsMeetingState> {\n unavailable()\n },\n async leave(): Promise<void> {\n unavailable()\n },\n async leaveAndAwait(_options: {timeoutMs: number}): Promise<{completed: boolean}> {\n unavailable()\n },\n async setMuted(_muted: boolean): Promise<AcsMeetingState> {\n unavailable()\n },\n async setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState> {\n unavailable()\n },\n async setAudioSource(_source: \"glasses\" | \"phone\"): Promise<AcsMeetingState> {\n unavailable()\n },\n async updateVideoSource(_whepUrl: string): Promise<void> {\n unavailable()\n },\n async restartVideoSource(): Promise<void> {\n unavailable()\n },\n async rebindSoftApIngest(): Promise<string> {\n unavailable()\n },\n async joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string> {\n unavailable()\n },\n async leaveScopedNetwork(): Promise<void> {},\n async probeScopedGateway(): Promise<{reachable: boolean; detail: string}> {\n return {reachable: false, detail: \"no scoped network on web\"}\n },\n async getState(): Promise<AcsMeetingState> {\n return {state: \"idle\", muted: false}\n },\n addListener() {\n return {remove() {}}\n },\n removeListeners() {},\n}\n"]}
|
package/ios/AcsFrameSender.swift
CHANGED
|
@@ -20,21 +20,23 @@ final class AcsFrameSender: NSObject {
|
|
|
20
20
|
stream.delegate = self
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/// True when the frame was handed to ACS; false when readiness, pacing or the send gate dropped it.
|
|
24
|
+
@discardableResult
|
|
25
|
+
func send(_ pixelBuffer: CVPixelBuffer) -> Bool {
|
|
24
26
|
stateLock.lock()
|
|
25
27
|
guard running, let stream else {
|
|
26
28
|
stateLock.unlock()
|
|
27
|
-
return
|
|
29
|
+
return false
|
|
28
30
|
}
|
|
29
31
|
let fps = stream.format.framesPerSecond
|
|
30
32
|
let now = CFAbsoluteTimeGetCurrent()
|
|
31
33
|
if lastSent > 0, now - lastSent < 1.0 / Double(max(fps, 1)) {
|
|
32
34
|
stateLock.unlock()
|
|
33
|
-
return
|
|
35
|
+
return false
|
|
34
36
|
}
|
|
35
37
|
guard let token = sendGate.acquire() else {
|
|
36
38
|
stateLock.unlock()
|
|
37
|
-
return
|
|
39
|
+
return false
|
|
38
40
|
}
|
|
39
41
|
lastSent = now
|
|
40
42
|
stateLock.unlock()
|
|
@@ -48,6 +50,7 @@ final class AcsFrameSender: NSObject {
|
|
|
48
50
|
sendGate.release(token)
|
|
49
51
|
if let error { NSLog("ACS-SPIKE send frame failed: \(error)") }
|
|
50
52
|
}
|
|
53
|
+
return true
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
func detach() {
|
|
@@ -164,6 +164,19 @@ public class AcsMeetingModule: Module {
|
|
|
164
164
|
session.setMuted(muted) { promise.resolve($0) }
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
AsyncFunction("setVideoEnabled") { (enabled: Bool, promise: Promise) in
|
|
168
|
+
guard let session = self.session else {
|
|
169
|
+
promise.reject(AcsMeetingError("No active meeting"))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
session.setVideoEnabled(enabled) { result in
|
|
173
|
+
switch result {
|
|
174
|
+
case let .success(state): promise.resolve(state)
|
|
175
|
+
case let .failure(error): promise.reject(error)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
AsyncFunction("setAudioSource") { (source: String, promise: Promise) in
|
|
168
181
|
guard let session = self.session else {
|
|
169
182
|
promise.resolve(["state": "idle", "muted": false, "audioSource": source]); return
|
|
@@ -236,6 +249,9 @@ final class AcsMeetingSession {
|
|
|
236
249
|
private lazy var applier = AudioPolicyApplier(controller: controller, scheduler: scheduler) { NSLog("ACS-SPIKE \($0)") }
|
|
237
250
|
private var phase = "idle"
|
|
238
251
|
private var muted = false
|
|
252
|
+
/// Whether ACS is sending `videoStream`. Only `setVideoEnabled` turns it off; every call starts on.
|
|
253
|
+
private var videoEnabled = true
|
|
254
|
+
private var videoStream: VirtualOutgoingVideoStream?
|
|
239
255
|
private var meetingUrl: String?
|
|
240
256
|
private var lastError: String?
|
|
241
257
|
private var callEndReason: (code: Int, subcode: Int)?
|
|
@@ -315,6 +331,7 @@ final class AcsMeetingSession {
|
|
|
315
331
|
var result: [String: Any] = [
|
|
316
332
|
"state": phase,
|
|
317
333
|
"muted": muted,
|
|
334
|
+
"videoEnabled": videoEnabled,
|
|
318
335
|
"provider": "acs-teams",
|
|
319
336
|
"audioSource": audioSource,
|
|
320
337
|
"activeStream": controller.readActive().rawValue,
|
|
@@ -471,6 +488,7 @@ final class AcsMeetingSession {
|
|
|
471
488
|
let videoOptions = RawOutgoingVideoStreamOptions()
|
|
472
489
|
videoOptions.formats = [videoFormat]
|
|
473
490
|
let videoStream = VirtualOutgoingVideoStream(videoStreamOptions: videoOptions)
|
|
491
|
+
self.videoStream = videoStream
|
|
474
492
|
frameSender = AcsFrameSender()
|
|
475
493
|
frameSender.attach(videoStream)
|
|
476
494
|
|
|
@@ -590,7 +608,12 @@ final class AcsMeetingSession {
|
|
|
590
608
|
self?.feedOutgoingPcm(pcm, sampleRate: rate, channels: channels, generation: generation)
|
|
591
609
|
}
|
|
592
610
|
let source: DecodedGlassesMediaSource = sourceConfig.kind == .softap ? LocalWhipIngestSource() : WhepVideoSource()
|
|
593
|
-
source.onFrame = { [frameSender] buffer in
|
|
611
|
+
source.onFrame = { [frameSender] buffer in
|
|
612
|
+
// Preview first, deliberately: the tap must see frames the ACS sender's pacing and
|
|
613
|
+
// readiness gates would otherwise hide, and it cannot delay or break this call.
|
|
614
|
+
DecodedFrameTap.shared.offer(buffer)
|
|
615
|
+
if frameSender.send(buffer) { DecodedFrameTap.shared.recordAcsSend() }
|
|
616
|
+
}
|
|
594
617
|
source.onPcm = { [weak self] pcm, rate, channels in
|
|
595
618
|
self?.feedOutgoingPcm(pcm, sampleRate: rate, channels: channels, generation: generation)
|
|
596
619
|
}
|
|
@@ -726,6 +749,46 @@ final class AcsMeetingSession {
|
|
|
726
749
|
}
|
|
727
750
|
}
|
|
728
751
|
|
|
752
|
+
/// Stop or resume the camera Teams receives without leaving the call. The glasses source, the
|
|
753
|
+
/// preview tap and the WHEP/WHIP transport keep running; `AcsFrameSender` drops frames while
|
|
754
|
+
/// ACS reports the stream stopped. `videoEnabled` only moves once ACS accepts the change.
|
|
755
|
+
func setVideoEnabled(_ next: Bool, completion: @escaping (Result<[String: Any], Error>) -> Void) {
|
|
756
|
+
queue.async {
|
|
757
|
+
guard let call = self.call, let stream = self.videoStream else {
|
|
758
|
+
completion(.failure(AcsMeetingError("No active meeting")))
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
guard self.videoEnabled != next else {
|
|
762
|
+
completion(.success(self.snapshotLocked()))
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
let generation = self.joinGeneration
|
|
766
|
+
let finished: (Error?) -> Void = { error in
|
|
767
|
+
self.queue.async {
|
|
768
|
+
guard self.joinGeneration == generation, self.call === call else {
|
|
769
|
+
completion(.failure(AcsMeetingError("The meeting ended before the camera changed")))
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
if let error {
|
|
773
|
+
NSLog("ACS-SPIKE setVideoEnabled=\(next) failed: \(error)")
|
|
774
|
+
completion(.failure(error))
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
NSLog("ACS-SPIKE setVideoEnabled=\(next)")
|
|
778
|
+
self.videoEnabled = next
|
|
779
|
+
let snapshot = self.snapshotLocked()
|
|
780
|
+
self.onState(snapshot)
|
|
781
|
+
completion(.success(snapshot))
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if next {
|
|
785
|
+
call.startVideo(stream: stream, completionHandler: finished)
|
|
786
|
+
} else {
|
|
787
|
+
call.stopVideo(stream: stream, completionHandler: finished)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
729
792
|
func setAudioSource(_ source: String, completion: @escaping ([String: Any]) -> Void) {
|
|
730
793
|
queue.async {
|
|
731
794
|
if AcsAudioPolicy.parseSource(source) == nil {
|
|
@@ -1014,6 +1077,8 @@ final class AcsMeetingSession {
|
|
|
1014
1077
|
pcmBridge = nil
|
|
1015
1078
|
outgoingReady = false
|
|
1016
1079
|
muted = false
|
|
1080
|
+
videoEnabled = true
|
|
1081
|
+
videoStream = nil
|
|
1017
1082
|
audioSource = "glasses"
|
|
1018
1083
|
lastSafety = .degraded
|
|
1019
1084
|
meetingUrl = nil
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/acs-meeting",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.329",
|
|
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.3.0-dev.
|
|
35
|
+
"@mentra/glasses-media": "3.3.0-dev.329"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"expo-module-scripts": "^55.0.2",
|
package/src/AcsMeeting.types.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type AcsMeetingParticipant = {
|
|
|
23
23
|
export type AcsMeetingState = {
|
|
24
24
|
state: MeetingPhase
|
|
25
25
|
muted: boolean
|
|
26
|
+
/** Whether ACS is sending the glasses camera. Resets to true on every join. */
|
|
27
|
+
videoEnabled?: boolean
|
|
26
28
|
error?: string
|
|
27
29
|
meetingUrl?: string
|
|
28
30
|
provider?: "acs-teams"
|
package/src/AcsMeetingModule.ts
CHANGED
|
@@ -19,6 +19,11 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
|
|
|
19
19
|
*/
|
|
20
20
|
leaveAndAwait(options: {timeoutMs: number}): Promise<{completed: boolean}>
|
|
21
21
|
setMuted(muted: boolean): Promise<AcsMeetingState>
|
|
22
|
+
/**
|
|
23
|
+
* Stop or resume the outgoing video stream (ACS `stopVideo`/`startVideo`). The call, the glasses
|
|
24
|
+
* media source and the preview tap stay up; frames are dropped before ACS while stopped.
|
|
25
|
+
*/
|
|
26
|
+
setVideoEnabled(enabled: boolean): Promise<AcsMeetingState>
|
|
22
27
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>
|
|
23
28
|
updateVideoSource(whepUrl: string): Promise<void>
|
|
24
29
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
@@ -24,6 +24,9 @@ export default {
|
|
|
24
24
|
async setMuted(_muted: boolean): Promise<AcsMeetingState> {
|
|
25
25
|
unavailable()
|
|
26
26
|
},
|
|
27
|
+
async setVideoEnabled(_enabled: boolean): Promise<AcsMeetingState> {
|
|
28
|
+
unavailable()
|
|
29
|
+
},
|
|
27
30
|
async setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState> {
|
|
28
31
|
unavailable()
|
|
29
32
|
},
|