@mentra/acs-meeting 3.2.0-dev.217 → 3.2.0-dev.222
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 +4 -2
- package/android/build.gradle +23 -0
- package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +291 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/mentra/acsmeeting/AbandonedCallAgent.kt +36 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +392 -22
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +909 -56
- package/android/src/main/java/com/mentra/acsmeeting/CallCapabilities.kt +33 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +21 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AudioUplinkChain.kt +126 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +27 -4
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +45 -10
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +110 -5
- package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +501 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +81 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +355 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +109 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +160 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +103 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +53 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +127 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +573 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +10 -101
- package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +147 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +47 -5
- package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +772 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +73 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +17 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +139 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +144 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +46 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +160 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +3 -1
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +69 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +207 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +356 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +155 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +94 -3
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +33 -5
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +15 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +178 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +145 -0
- package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +143 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +23 -4
- package/android/src/main/java/com/mentra/acsmeeting/video/FramePacer.kt +70 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +1 -1
- package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +36 -3
- package/android/src/test/java/com/mentra/acsmeeting/AbandonedCallAgentTest.kt +74 -0
- package/android/src/test/java/com/mentra/acsmeeting/CallCapabilitiesTest.kt +47 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AudioUplinkChainTest.kt +250 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +44 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +34 -4
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +96 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +102 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +148 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +59 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +59 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +35 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +165 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +96 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +187 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +172 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +141 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +69 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +40 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +41 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +26 -8
- package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +109 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +165 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +148 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +76 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +275 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +9 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +356 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +411 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +83 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +69 -3
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +182 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +149 -0
- package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +97 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +34 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/FramePacerTest.kt +105 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +5 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +38 -3
- package/build/AcsMeeting.types.d.ts +5 -0
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +23 -0
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/build/AcsMeetingModule.web.d.ts +15 -0
- package/build/AcsMeetingModule.web.d.ts.map +1 -1
- package/build/AcsMeetingModule.web.js +13 -0
- package/build/AcsMeetingModule.web.js.map +1 -1
- package/ios/AcsMeetingModule.swift +213 -2
- package/package.json +1 -1
- package/src/AcsMeeting.types.ts +5 -0
- package/src/AcsMeetingModule.ts +13 -0
- package/src/AcsMeetingModule.web.ts +13 -0
|
@@ -2,12 +2,35 @@ import { NativeModule } from "expo";
|
|
|
2
2
|
import type { AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState } from "./AcsMeeting.types";
|
|
3
3
|
declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {
|
|
4
4
|
join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>;
|
|
5
|
+
/** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */
|
|
6
|
+
prepareAgent(options: {
|
|
7
|
+
token: string;
|
|
8
|
+
displayName?: string;
|
|
9
|
+
}): Promise<AcsMeetingState>;
|
|
5
10
|
leave(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Leave, and resolve only once the hang-up, the agent disposal, and the network releases have
|
|
13
|
+
* finished. `leave()` only queues that work, so awaiting it proves nothing about cleanup.
|
|
14
|
+
*/
|
|
15
|
+
leaveAndAwait(options: {
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
completed: boolean;
|
|
19
|
+
}>;
|
|
6
20
|
setMuted(muted: boolean): Promise<AcsMeetingState>;
|
|
7
21
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
8
22
|
updateVideoSource(whepUrl: string): Promise<void>;
|
|
9
23
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
10
24
|
restartVideoSource(): Promise<void>;
|
|
25
|
+
/** SoftAP: join the glasses hotspot as a scoped network; resolves to the phone's IPv4 on it. Android only. */
|
|
26
|
+
joinScopedNetwork(ssid: string, passphrase: string): Promise<string>;
|
|
27
|
+
beginTrace(traceId: string): Promise<void>;
|
|
28
|
+
leaveScopedNetwork(): Promise<void>;
|
|
29
|
+
/** SoftAP: TCP-probe the hotspot gateway over the scoped network. */
|
|
30
|
+
probeScopedGateway(): Promise<{
|
|
31
|
+
reachable: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
}>;
|
|
11
34
|
getState(): Promise<AcsMeetingState>;
|
|
12
35
|
}
|
|
13
36
|
declare const _default: AcsMeetingNativeModule;
|
|
@@ -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,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,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,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,8GAA8G;IAC9G,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACpE,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1C,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IACnC,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;AA4BtD,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. `leave()` only queues that work, so awaiting it proves nothing about 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 /** SoftAP: join the glasses hotspot as a scoped network; resolves to the phone's IPv4 on it. Android only. */\n joinScopedNetwork(ssid: string, passphrase: string): Promise<string>\n beginTrace(traceId: string): Promise<void>\n leaveScopedNetwork(): Promise<void>\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,11 +1,26 @@
|
|
|
1
1
|
import type { AcsMeetingJoinOptions, AcsMeetingState } from "./AcsMeeting.types";
|
|
2
2
|
declare const _default: {
|
|
3
3
|
join(_options: AcsMeetingJoinOptions): Promise<AcsMeetingState>;
|
|
4
|
+
prepareAgent(_options: {
|
|
5
|
+
token: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
}): Promise<AcsMeetingState>;
|
|
4
8
|
leave(): Promise<void>;
|
|
9
|
+
leaveAndAwait(_options: {
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
completed: boolean;
|
|
13
|
+
}>;
|
|
5
14
|
setMuted(_muted: boolean): Promise<AcsMeetingState>;
|
|
6
15
|
setAudioSource(_source: "glasses" | "phone"): Promise<AcsMeetingState>;
|
|
7
16
|
updateVideoSource(_whepUrl: string): Promise<void>;
|
|
8
17
|
restartVideoSource(): Promise<void>;
|
|
18
|
+
joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string>;
|
|
19
|
+
leaveScopedNetwork(): Promise<void>;
|
|
20
|
+
probeScopedGateway(): Promise<{
|
|
21
|
+
reachable: boolean;
|
|
22
|
+
detail: string;
|
|
23
|
+
}>;
|
|
9
24
|
getState(): Promise<AcsMeetingState>;
|
|
10
25
|
addListener(): {
|
|
11
26
|
remove(): 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;
|
|
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;6BAGV,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;;;;;;AAhC5C,wBAuCC"}
|
|
@@ -5,9 +5,15 @@ export default {
|
|
|
5
5
|
async join(_options) {
|
|
6
6
|
unavailable();
|
|
7
7
|
},
|
|
8
|
+
async prepareAgent(_options) {
|
|
9
|
+
unavailable();
|
|
10
|
+
},
|
|
8
11
|
async leave() {
|
|
9
12
|
unavailable();
|
|
10
13
|
},
|
|
14
|
+
async leaveAndAwait(_options) {
|
|
15
|
+
unavailable();
|
|
16
|
+
},
|
|
11
17
|
async setMuted(_muted) {
|
|
12
18
|
unavailable();
|
|
13
19
|
},
|
|
@@ -20,6 +26,13 @@ export default {
|
|
|
20
26
|
async restartVideoSource() {
|
|
21
27
|
unavailable();
|
|
22
28
|
},
|
|
29
|
+
async joinScopedNetwork(_ssid, _passphrase) {
|
|
30
|
+
unavailable();
|
|
31
|
+
},
|
|
32
|
+
async leaveScopedNetwork() { },
|
|
33
|
+
async probeScopedGateway() {
|
|
34
|
+
return { reachable: false, detail: "no scoped network on web" };
|
|
35
|
+
},
|
|
23
36
|
async getState() {
|
|
24
37
|
return { state: "idle", muted: false };
|
|
25
38
|
},
|
|
@@ -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,KAAK;QACT,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,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 leave(): Promise<void> {\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 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,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"]}
|
|
@@ -14,7 +14,7 @@ public class AcsMeetingModule: Module {
|
|
|
14
14
|
AsyncFunction("join") { (options: [String: Any]) in
|
|
15
15
|
let token = try requireString(options, "token")
|
|
16
16
|
let meetingUrl = try requireString(options, "meetingUrl")
|
|
17
|
-
let whepUrl = try
|
|
17
|
+
let whepUrl = try requireWhepUrl(options)
|
|
18
18
|
let displayName = options["displayName"] as? String
|
|
19
19
|
let dumpWav = options["dumpPcmWav"] as? Bool ?? false
|
|
20
20
|
let audioSource = options["audioSource"] as? String ?? "glasses"
|
|
@@ -30,10 +30,62 @@ public class AcsMeetingModule: Module {
|
|
|
30
30
|
return meeting.snapshot()
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// Refuses rather than no-ops. A silent success here would let the orchestrator go on to an ACS
|
|
34
|
+
/// join and a glasses publish that have no network to meet on, which surfaces as a black tile
|
|
35
|
+
/// several steps later instead of as the unsupported transport it is.
|
|
36
|
+
AsyncFunction("joinScopedNetwork") { (_: String, _: String) -> String in
|
|
37
|
+
throw NSError(
|
|
38
|
+
domain: "MentraAcsMeeting",
|
|
39
|
+
code: 2,
|
|
40
|
+
userInfo: [
|
|
41
|
+
NSLocalizedDescriptionKey:
|
|
42
|
+
"NOT_IMPLEMENTED: SoftAP calling is Android-only; iOS cannot join the glasses hotspot as a scoped network",
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
AsyncFunction("leaveScopedNetwork") {
|
|
48
|
+
// Nothing was ever joined, so releasing is a no-op rather than an error: teardown must stay
|
|
49
|
+
// safe to run after a failed start.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
AsyncFunction("probeScopedGateway") { () -> [String: Any] in
|
|
53
|
+
// No scoped network exists on iOS, so there is nothing to probe. Report it as such rather
|
|
54
|
+
// than as unreachable, which the orchestrator would read as a live network failure.
|
|
55
|
+
["reachable": false, "detail": "no scoped network on iOS"]
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
AsyncFunction("leave") {
|
|
34
59
|
self.session?.leave()
|
|
35
60
|
}
|
|
36
61
|
|
|
62
|
+
AsyncFunction("endForEveryone") { (promise: Promise) in
|
|
63
|
+
guard let session = self.session else {
|
|
64
|
+
promise.reject(NSError(
|
|
65
|
+
domain: "MentraAcsMeeting",
|
|
66
|
+
code: 3,
|
|
67
|
+
userInfo: [NSLocalizedDescriptionKey: "No active meeting to end"]
|
|
68
|
+
))
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
session.endForEveryone { error in
|
|
72
|
+
// Local teardown has already run by the time this fires, so a rejection here means
|
|
73
|
+
// "the meeting may still be running", never "you are still in it".
|
|
74
|
+
if let error {
|
|
75
|
+
promise.reject(error)
|
|
76
|
+
} else {
|
|
77
|
+
promise.resolve(session.snapshot())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
AsyncFunction("scopedNetworkInfo") { () -> [String: Any?] in
|
|
83
|
+
// No scoped network on iOS, so there is no prefix for the ICE-path proof to check
|
|
84
|
+
// against. Reported as unavailable rather than as an empty prefix, which the proof
|
|
85
|
+
// would read as "could not describe the network" on a platform that has none.
|
|
86
|
+
["available": false, "localIpv4": nil, "prefix": nil]
|
|
87
|
+
}
|
|
88
|
+
|
|
37
89
|
AsyncFunction("setMuted") { (muted: Bool) in
|
|
38
90
|
self.session?.setMuted(muted) ?? ["state": "idle", "muted": muted]
|
|
39
91
|
}
|
|
@@ -114,10 +166,16 @@ final class AcsMeetingSession {
|
|
|
114
166
|
private static let mediaRestartBaseMs = 1000
|
|
115
167
|
private static let mediaRestartMaxMs = 10000
|
|
116
168
|
private var joinGeneration: UInt64 = 0
|
|
169
|
+
private var capabilitiesFeature: CapabilitiesCallFeature?
|
|
170
|
+
/// nil means "not reported yet", which the miniapp shows as End disabled rather than absent.
|
|
171
|
+
private var hangUpForEveryone: (allowed: Bool, reason: String)?
|
|
117
172
|
private lazy var callDelegateProxy = AcsCallDelegateProxy(
|
|
118
173
|
onStateChange: { [weak self] call in self?.handleCallStateChange(call) },
|
|
119
174
|
onMuteChange: { [weak self] call in self?.handleCallMuteChange(call) }
|
|
120
175
|
)
|
|
176
|
+
private lazy var capabilitiesDelegateProxy = AcsCapabilitiesDelegateProxy(
|
|
177
|
+
onChanged: { [weak self] in self?.refreshCapabilities() }
|
|
178
|
+
)
|
|
121
179
|
|
|
122
180
|
init(onState: @escaping ([String: Any]) -> Void, onIncomingPcm: @escaping (String, Int, Int) -> Void) {
|
|
123
181
|
self.onState = onState
|
|
@@ -134,6 +192,15 @@ final class AcsMeetingSession {
|
|
|
134
192
|
"audioSafety": lastSafety.rawValue,
|
|
135
193
|
"mediaSource": mediaSource.rawValue,
|
|
136
194
|
]
|
|
195
|
+
// Always present, so a host that simply has not heard from Teams yet is distinguishable
|
|
196
|
+
// from one that cannot report capabilities at all. Keys are omitted rather than sent as
|
|
197
|
+
// null while unknown; the host parses a missing key as unknown.
|
|
198
|
+
var hangUp: [String: Any] = [:]
|
|
199
|
+
if let capability = hangUpForEveryone {
|
|
200
|
+
hangUp["allowed"] = capability.allowed
|
|
201
|
+
hangUp["reason"] = capability.reason
|
|
202
|
+
}
|
|
203
|
+
result["capabilities"] = ["hangUpForEveryone": hangUp]
|
|
137
204
|
if let meetingUrl { result["meetingUrl"] = meetingUrl }
|
|
138
205
|
if let lastError { result["error"] = lastError }
|
|
139
206
|
return result
|
|
@@ -314,6 +381,7 @@ final class AcsMeetingSession {
|
|
|
314
381
|
}
|
|
315
382
|
self.call = call
|
|
316
383
|
call.delegate = callDelegateProxy
|
|
384
|
+
attachCapabilities(call)
|
|
317
385
|
|
|
318
386
|
let bridge = PcmBridge(dumpWav: dumpWav)
|
|
319
387
|
pcmBridge = bridge
|
|
@@ -423,6 +491,102 @@ final class AcsMeetingSession {
|
|
|
423
491
|
queue.async { self.leaveLocked() }
|
|
424
492
|
}
|
|
425
493
|
|
|
494
|
+
/**
|
|
495
|
+
End the meeting for everyone, then tear this device down.
|
|
496
|
+
|
|
497
|
+
Local teardown runs whether or not ACS accepted the hang-up, and the error is reported after
|
|
498
|
+
it. The wearer is out of the call either way; what the caller learns from a rejection is only
|
|
499
|
+
that the others may still be in it.
|
|
500
|
+
|
|
501
|
+
A known-denied capability is refused locally rather than sent. Teams only lets a presenter end
|
|
502
|
+
a meeting for everyone, ACS rejects the attempt with an opaque error, and the wearer would have
|
|
503
|
+
sat through a confirm sheet for nothing. An *unknown* capability is not a refusal: it is the
|
|
504
|
+
ordinary state before the first capabilities event, and letting ACS answer is more honest than
|
|
505
|
+
guessing.
|
|
506
|
+
*/
|
|
507
|
+
func endForEveryone(completion: @escaping (Error?) -> Void) {
|
|
508
|
+
queue.async { [weak self] in
|
|
509
|
+
guard let self else { return }
|
|
510
|
+
if let refusal = self.hangUpForEveryoneRefusal() {
|
|
511
|
+
completion(NSError(
|
|
512
|
+
domain: "MentraAcsMeeting",
|
|
513
|
+
code: 3,
|
|
514
|
+
userInfo: [NSLocalizedDescriptionKey: refusal]
|
|
515
|
+
))
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
guard let active = self.call else {
|
|
519
|
+
self.leaveLocked()
|
|
520
|
+
completion(NSError(
|
|
521
|
+
domain: "MentraAcsMeeting",
|
|
522
|
+
code: 3,
|
|
523
|
+
userInfo: [NSLocalizedDescriptionKey: "No active meeting to end"]
|
|
524
|
+
))
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
let options = HangUpOptions()
|
|
528
|
+
options.forEveryone = true
|
|
529
|
+
active.hangUp(options: options) { [weak self] error in
|
|
530
|
+
guard let self else {
|
|
531
|
+
completion(error)
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
if let error {
|
|
535
|
+
NSLog("ACS-SPIKE endForEveryone failed: \(error)")
|
|
536
|
+
}
|
|
537
|
+
self.queue.async {
|
|
538
|
+
self.leaveLocked()
|
|
539
|
+
completion(error)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/// The refusal to report, or nil when the End should be attempted.
|
|
546
|
+
private func hangUpForEveryoneRefusal() -> String? {
|
|
547
|
+
guard let capability = readHangUpForEveryone(), !capability.allowed else { return nil }
|
|
548
|
+
return "hang_up_for_everyone_not_allowed:\(capability.reason)"
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
Subscribe to the runtime capability that decides whether End is offered.
|
|
553
|
+
|
|
554
|
+
Subscribed rather than read once: Teams can grant the capability after admission (a wearer
|
|
555
|
+
promoted to presenter mid-call), and an End button that never turns on is the same bug as one
|
|
556
|
+
that lies about what it does.
|
|
557
|
+
*/
|
|
558
|
+
private func attachCapabilities(_ call: Call) {
|
|
559
|
+
let feature = call.feature(Features.capabilities)
|
|
560
|
+
capabilitiesFeature = feature
|
|
561
|
+
feature.delegate = capabilitiesDelegateProxy
|
|
562
|
+
refreshCapabilities()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
fileprivate func refreshCapabilities() {
|
|
566
|
+
queue.async { [weak self] in
|
|
567
|
+
guard let self else { return }
|
|
568
|
+
let next = self.readHangUpForEveryone()
|
|
569
|
+
guard next?.allowed != self.hangUpForEveryone?.allowed
|
|
570
|
+
|| next?.reason != self.hangUpForEveryone?.reason else { return }
|
|
571
|
+
self.hangUpForEveryone = next
|
|
572
|
+
NSLog("ACS-SPIKE hangUpForEveryone allowed=\(next?.allowed.description ?? "unknown") reason=\(next?.reason ?? "-")")
|
|
573
|
+
self.onState(self.snapshot())
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/// Drop the capability so the next call starts unknown instead of inheriting the last one's.
|
|
578
|
+
private func detachCapabilities() {
|
|
579
|
+
capabilitiesFeature?.delegate = nil
|
|
580
|
+
capabilitiesFeature = nil
|
|
581
|
+
hangUpForEveryone = nil
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private func readHangUpForEveryone() -> (allowed: Bool, reason: String)? {
|
|
585
|
+
guard let feature = capabilitiesFeature else { return nil }
|
|
586
|
+
guard let capability = feature.capabilities.first(where: { $0.type == .hangUpForEveryone }) else { return nil }
|
|
587
|
+
return (capability.isAllowed, String(describing: capability.reason))
|
|
588
|
+
}
|
|
589
|
+
|
|
426
590
|
fileprivate func applyAudioPolicy(_ reason: String) {
|
|
427
591
|
queue.async { self.applyAudioPolicyOnQueue(reason) }
|
|
428
592
|
}
|
|
@@ -469,6 +633,7 @@ final class AcsMeetingSession {
|
|
|
469
633
|
// Detach before stop so the teardown's own idle transition does not emit a
|
|
470
634
|
// snapshot (or schedule a rebuild) for a call that is going away.
|
|
471
635
|
cancelMediaRestart()
|
|
636
|
+
detachCapabilities()
|
|
472
637
|
whep?.onStateChange = nil
|
|
473
638
|
mediaSource = .idle
|
|
474
639
|
whep?.stop()
|
|
@@ -644,6 +809,21 @@ private final class AcsCallDelegateProxy: NSObject, CallDelegate {
|
|
|
644
809
|
}
|
|
645
810
|
}
|
|
646
811
|
|
|
812
|
+
private final class AcsCapabilitiesDelegateProxy: NSObject, CapabilitiesCallFeatureDelegate {
|
|
813
|
+
private let onChanged: () -> Void
|
|
814
|
+
|
|
815
|
+
init(onChanged: @escaping () -> Void) {
|
|
816
|
+
self.onChanged = onChanged
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
func capabilitiesCallFeature(
|
|
820
|
+
_: CapabilitiesCallFeature,
|
|
821
|
+
didChangeCapabilities _: CapabilitiesChangedEventArgs
|
|
822
|
+
) {
|
|
823
|
+
onChanged()
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
647
827
|
private struct AcsMeetingError: LocalizedError {
|
|
648
828
|
let message: String
|
|
649
829
|
|
|
@@ -675,7 +855,7 @@ struct AcsOutgoingVideo {
|
|
|
675
855
|
let fps: Int
|
|
676
856
|
let maxBitrateBps: Int
|
|
677
857
|
|
|
678
|
-
static let hd = AcsOutgoingVideo(width: 1280, height: 720, fps: 15, maxBitrateBps:
|
|
858
|
+
static let hd = AcsOutgoingVideo(width: 1280, height: 720, fps: 15, maxBitrateBps: 3_000_000)
|
|
679
859
|
static let allowedSizes: Set<String> = ["1280x720", "960x540"]
|
|
680
860
|
}
|
|
681
861
|
|
|
@@ -686,6 +866,37 @@ private func requireString(_ options: [String: Any], _ key: String) throws -> St
|
|
|
686
866
|
return value
|
|
687
867
|
}
|
|
688
868
|
|
|
869
|
+
/// Resolves the WHEP URL from either the `videoSource` union or a legacy bare `whepUrl`.
|
|
870
|
+
///
|
|
871
|
+
/// SoftAP is refused here with `NOT_IMPLEMENTED` rather than allowed to fall through. Joining
|
|
872
|
+
/// without a usable video source would produce a call that connects and shows the remote
|
|
873
|
+
/// participant a black tile, with nothing in the logs to explain it. iOS cannot serve the local
|
|
874
|
+
/// WHIP endpoint yet — see the SoftAP plan's iOS spike — so saying so plainly is the correct
|
|
875
|
+
/// behaviour until it can.
|
|
876
|
+
private func requireWhepUrl(_ options: [String: Any]) throws -> String {
|
|
877
|
+
guard let source = options["videoSource"] as? [String: Any] else {
|
|
878
|
+
return try requireString(options, "whepUrl")
|
|
879
|
+
}
|
|
880
|
+
let type = (source["type"] as? String)?.lowercased() ?? ""
|
|
881
|
+
switch type {
|
|
882
|
+
case "whep":
|
|
883
|
+
if let url = source["url"] as? String, !url.isEmpty { return url }
|
|
884
|
+
return try requireString(options, "whepUrl")
|
|
885
|
+
case "softap":
|
|
886
|
+
throw NSError(
|
|
887
|
+
domain: "MentraAcsMeeting",
|
|
888
|
+
code: 2,
|
|
889
|
+
userInfo: [NSLocalizedDescriptionKey: "NOT_IMPLEMENTED: SoftAP calling is Android-only"]
|
|
890
|
+
)
|
|
891
|
+
default:
|
|
892
|
+
throw NSError(
|
|
893
|
+
domain: "MentraAcsMeeting",
|
|
894
|
+
code: 1,
|
|
895
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported videoSource.type: \(type)"]
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
689
900
|
private func parseAcsOutgoingVideo(_ raw: Any?) throws -> AcsOutgoingVideo {
|
|
690
901
|
guard let raw else { return .hd }
|
|
691
902
|
guard let map = raw as? [String: Any] else {
|
package/package.json
CHANGED
package/src/AcsMeeting.types.ts
CHANGED
|
@@ -48,6 +48,11 @@ export type AcsMeetingJoinOptions = {
|
|
|
48
48
|
displayName?: string
|
|
49
49
|
/** "glasses" sends WHEP PCM. "phone" uses the ACS local mic (handset or BT). */
|
|
50
50
|
audioSource?: AcsAudioSource
|
|
51
|
+
/**
|
|
52
|
+
* Hold outgoing PCM this many ms before ACS. SoftAP+LC3 sets this in the host
|
|
53
|
+
* because BLE audio reaches the phone before SoftAP video.
|
|
54
|
+
*/
|
|
55
|
+
audioDelayMs?: number
|
|
51
56
|
/** Dump WHEP PCM to a WAV in cache for P4 verification. */
|
|
52
57
|
dumpPcmWav?: boolean
|
|
53
58
|
/** When omitted, native keeps 1280×720@15 / 2.5 Mbps. */
|
package/src/AcsMeetingModule.ts
CHANGED
|
@@ -4,12 +4,25 @@ import type {AcsMeetingJoinOptions, AcsMeetingModuleEvents, AcsMeetingState} fro
|
|
|
4
4
|
|
|
5
5
|
declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents> {
|
|
6
6
|
join(options: AcsMeetingJoinOptions): Promise<AcsMeetingState>
|
|
7
|
+
/** Sign in to ACS before SoftAP so Teams is not resolved through glasses DNS. */
|
|
8
|
+
prepareAgent(options: {token: string; displayName?: string}): Promise<AcsMeetingState>
|
|
7
9
|
leave(): Promise<void>
|
|
10
|
+
/**
|
|
11
|
+
* Leave, and resolve only once the hang-up, the agent disposal, and the network releases have
|
|
12
|
+
* finished. `leave()` only queues that work, so awaiting it proves nothing about cleanup.
|
|
13
|
+
*/
|
|
14
|
+
leaveAndAwait(options: {timeoutMs: number}): Promise<{completed: boolean}>
|
|
8
15
|
setMuted(muted: boolean): Promise<AcsMeetingState>
|
|
9
16
|
setAudioSource(source: "glasses" | "phone"): Promise<AcsMeetingState>
|
|
10
17
|
updateVideoSource(whepUrl: string): Promise<void>
|
|
11
18
|
/** Force a WHEP rebuild on the current URL (phone changed networks). */
|
|
12
19
|
restartVideoSource(): Promise<void>
|
|
20
|
+
/** SoftAP: join the glasses hotspot as a scoped network; resolves to the phone's IPv4 on it. Android only. */
|
|
21
|
+
joinScopedNetwork(ssid: string, passphrase: string): Promise<string>
|
|
22
|
+
beginTrace(traceId: string): Promise<void>
|
|
23
|
+
leaveScopedNetwork(): Promise<void>
|
|
24
|
+
/** SoftAP: TCP-probe the hotspot gateway over the scoped network. */
|
|
25
|
+
probeScopedGateway(): Promise<{reachable: boolean; detail: string}>
|
|
13
26
|
getState(): Promise<AcsMeetingState>
|
|
14
27
|
}
|
|
15
28
|
|
|
@@ -8,9 +8,15 @@ 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> {
|
|
12
|
+
unavailable()
|
|
13
|
+
},
|
|
11
14
|
async leave(): Promise<void> {
|
|
12
15
|
unavailable()
|
|
13
16
|
},
|
|
17
|
+
async leaveAndAwait(_options: {timeoutMs: number}): Promise<{completed: boolean}> {
|
|
18
|
+
unavailable()
|
|
19
|
+
},
|
|
14
20
|
async setMuted(_muted: boolean): Promise<AcsMeetingState> {
|
|
15
21
|
unavailable()
|
|
16
22
|
},
|
|
@@ -23,6 +29,13 @@ export default {
|
|
|
23
29
|
async restartVideoSource(): Promise<void> {
|
|
24
30
|
unavailable()
|
|
25
31
|
},
|
|
32
|
+
async joinScopedNetwork(_ssid: string, _passphrase: string): Promise<string> {
|
|
33
|
+
unavailable()
|
|
34
|
+
},
|
|
35
|
+
async leaveScopedNetwork(): Promise<void> {},
|
|
36
|
+
async probeScopedGateway(): Promise<{reachable: boolean; detail: string}> {
|
|
37
|
+
return {reachable: false, detail: "no scoped network on web"}
|
|
38
|
+
},
|
|
26
39
|
async getState(): Promise<AcsMeetingState> {
|
|
27
40
|
return {state: "idle", muted: false}
|
|
28
41
|
},
|