@mentra/acs-meeting 3.2.0-dev.222 → 3.2.0-dev.225

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.
Files changed (106) hide show
  1. package/README.md +14 -3
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +10 -10
  4. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +37 -37
  5. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +4 -4
  6. package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +3 -0
  7. package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +12 -10
  8. package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +2 -0
  9. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +2 -0
  10. package/android/src/test/java/com/mentra/acsmeeting/source/{AcsInvestigationTest.kt → MediaDiagnosticsTest.kt} +10 -10
  11. package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +3 -0
  12. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +2 -2
  13. package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +2 -0
  14. package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +2 -2
  15. package/build/AcsMeeting.types.d.ts +10 -1
  16. package/build/AcsMeeting.types.d.ts.map +1 -1
  17. package/build/AcsMeeting.types.js.map +1 -1
  18. package/build/AcsMeetingModule.d.ts +2 -2
  19. package/build/AcsMeetingModule.d.ts.map +1 -1
  20. package/build/AcsMeetingModule.js.map +1 -1
  21. package/ios/AcsMeeting.podspec +1 -1
  22. package/ios/AcsMeetingModule.swift +284 -148
  23. package/ios/PolicyKit/Package.swift +2 -1
  24. package/ios/PolicyKit/Sources/AcsAudioPolicy/CallbackOperation.swift +60 -0
  25. package/ios/PolicyKit/Sources/AcsAudioPolicy/CapturePolicy.swift +5 -0
  26. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +1 -0
  27. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/CallAgentRetirementTests.swift +113 -0
  28. package/package.json +3 -2
  29. package/src/AcsMeeting.types.ts +4 -1
  30. package/src/AcsMeetingModule.ts +2 -2
  31. package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +0 -291
  32. package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +0 -501
  33. package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +0 -51
  34. package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +0 -81
  35. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +0 -355
  36. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +0 -109
  37. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +0 -160
  38. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +0 -103
  39. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +0 -53
  40. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +0 -127
  41. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +0 -573
  42. package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +0 -99
  43. package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +0 -487
  44. package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +0 -147
  45. package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +0 -45
  46. package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +0 -143
  47. package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +0 -772
  48. package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +0 -57
  49. package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +0 -17
  50. package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +0 -139
  51. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +0 -144
  52. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +0 -46
  53. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +0 -160
  54. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +0 -205
  55. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +0 -214
  56. package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +0 -33
  57. package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +0 -110
  58. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +0 -207
  59. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +0 -356
  60. package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +0 -155
  61. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +0 -78
  62. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +0 -372
  63. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +0 -79
  64. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +0 -15
  65. package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +0 -48
  66. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +0 -178
  67. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +0 -145
  68. package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +0 -143
  69. package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +0 -22
  70. package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +0 -72
  71. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +0 -102
  72. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +0 -148
  73. package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +0 -59
  74. package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +0 -59
  75. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +0 -35
  76. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +0 -165
  77. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +0 -96
  78. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +0 -187
  79. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +0 -172
  80. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +0 -141
  81. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +0 -69
  82. package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +0 -22
  83. package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +0 -41
  84. package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +0 -60
  85. package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +0 -151
  86. package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +0 -67
  87. package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +0 -165
  88. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +0 -148
  89. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +0 -76
  90. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +0 -275
  91. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +0 -193
  92. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +0 -176
  93. package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +0 -45
  94. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +0 -356
  95. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +0 -411
  96. package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +0 -83
  97. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +0 -61
  98. package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +0 -322
  99. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +0 -18
  100. package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +0 -49
  101. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +0 -149
  102. package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +0 -97
  103. package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +0 -33
  104. package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +0 -126
  105. package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +0 -68
  106. package/ios/WhepVideoSource.swift +0 -356
package/README.md CHANGED
@@ -391,6 +391,15 @@ rollback; it takes effect on the next join.
391
391
  `scripts/acs-ladder.ts` in the Mentra-Call repo parses these lines and prints pass/fail
392
392
  over a trailing 10-second window, including the `recv`-vs-`dec` attribution.
393
393
 
394
+ ## Shared media transport
395
+
396
+ The native receivers, decoded buffers, local WHIP server, and network helpers live in
397
+ [`glasses-media`](../glasses-media/README.md). ACS consumes that library on both platforms.
398
+ The iOS SoftAP path uses the existing persistent hotspot-join approach, receives WebRTC
399
+ locally, and feeds the ACS raw outgoing streams. `join` resolves with `ingestUrl` only
400
+ once the receiver is ready; `leaveAndAwait` waits for native cleanup. Physical routing,
401
+ screen-off streaming, and thermal qualification remain device checks.
402
+
394
403
  ## iOS host setup
395
404
 
396
405
  Add `"@mentra/acs-meeting"` to the host's Expo `plugins` list, then run
@@ -406,7 +415,7 @@ The host's other pods retain their configured linkage.
406
415
  ## Tests
407
416
 
408
417
  ```bash
409
- cd mobile/android && ./gradlew :mentra-acs-meeting:testDebugUnitTest
418
+ ./scripts/check-android-compile.sh bluetooth-sdk :mentra-acs-meeting:testDebugUnitTest :mentra-glasses-media:testDebugUnitTest
410
419
  ```
411
420
 
412
421
  These are plain JVM tests with no Robolectric. That constrains what can be tested: ACS SDK
@@ -426,8 +435,10 @@ counter race that caused the false conservation failures.
426
435
  whether the glasses are actually producing 15 fps.
427
436
  - **Outgoing glasses PCM reads as silence** (`P4 pcm meanAbs=0`) and needs its own
428
437
  investigation.
429
- - **iOS is foreground-only.** The Swift side under `ios/` does not have the telemetry
430
- ladder, and Android's audio-routing answers do not transfer re-verify separately.
438
+ - **iOS media qualification needs device coverage.** Screen-off operation and background
439
+ gallery transfers already work. Validate sustained local WebRTC reception and ACS
440
+ publishing on that existing background infrastructure. The Swift side does not yet
441
+ have the Android telemetry ladder.
431
442
  iOS also has no `RemoteRoster` yet (`participants` is never emitted) and does not apply
432
443
  `maxBitrateBps` to the outgoing stream.
433
444
 
@@ -66,6 +66,7 @@ afterEvaluate {
66
66
  }
67
67
 
68
68
  dependencies {
69
+ implementation project(':mentra-glasses-media')
69
70
  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
70
71
  implementation 'com.squareup.okhttp3:okhttp:4.12.0'
71
72
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
@@ -5,12 +5,12 @@ import android.content.Intent
5
5
  import android.os.Build
6
6
  import android.os.SystemClock
7
7
  import android.provider.Settings
8
- import com.mentra.acsmeeting.network.InternetHold
9
- import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector
10
- import com.mentra.acsmeeting.network.ScopedNetworkError
11
- import com.mentra.acsmeeting.network.ScopedSoftApNetwork
8
+ import com.mentra.glassesmedia.network.InternetHold
9
+ import com.mentra.glassesmedia.network.ScopedNetworkChangeDetector
10
+ import com.mentra.glassesmedia.network.ScopedNetworkError
11
+ import com.mentra.glassesmedia.network.ScopedSoftApNetwork
12
12
  import com.mentra.acsmeeting.source.MeetingVideoSourceSpec
13
- import com.mentra.acsmeeting.trace.SoftApTrace
13
+ import com.mentra.glassesmedia.trace.SoftApTrace
14
14
  import com.mentra.acsmeeting.video.VideoProfile
15
15
  import expo.modules.kotlin.modules.Module
16
16
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -104,7 +104,7 @@ class AcsMeetingModule : Module() {
104
104
 
105
105
  AsyncFunction("beginTrace") { traceId: String ->
106
106
  require(traceId.matches(Regex("[A-Za-z0-9]*"))) { "invalid trace id" }
107
- com.mentra.acsmeeting.trace.SoftApTrace.begin(traceId)
107
+ com.mentra.glassesmedia.trace.SoftApTrace.begin(traceId)
108
108
  Unit
109
109
  }
110
110
 
@@ -133,7 +133,7 @@ class AcsMeetingModule : Module() {
133
133
  override fun onAvailable(network: android.net.Network, localIpv4: String) = Unit
134
134
 
135
135
  override fun onLost(error: ScopedNetworkError) {
136
- com.mentra.acsmeeting.trace.SoftApTrace.failure("scoped_network_lost_midcall", "code" to error.code)
136
+ com.mentra.glassesmedia.trace.SoftApTrace.failure("scoped_network_lost_midcall", "code" to error.code)
137
137
  sendEvent(
138
138
  "onScopedNetworkLost",
139
139
  mapOf("code" to error.code, "message" to (error.message ?: "The glasses hotspot went away")),
@@ -147,9 +147,9 @@ class AcsMeetingModule : Module() {
147
147
  // then a retry reminted ACS after the user turned Wi-Fi on — and Android often made
148
148
  // that new Wi-Fi the default route with no internet, so token mint hung on DNS.
149
149
  promptToEnableWifi()
150
- com.mentra.acsmeeting.trace.SoftApTrace.stage("wifi_enable_wait", "timeoutMs" to ScopedSoftApNetwork.WIFI_ENABLE_WAIT_MS)
150
+ com.mentra.glassesmedia.trace.SoftApTrace.stage("wifi_enable_wait", "timeoutMs" to ScopedSoftApNetwork.WIFI_ENABLE_WAIT_MS)
151
151
  val enabled = scoped.awaitWifiEnabled()
152
- com.mentra.acsmeeting.trace.SoftApTrace.stage(
152
+ com.mentra.glassesmedia.trace.SoftApTrace.stage(
153
153
  "wifi_enable_wait_done",
154
154
  "enabled" to enabled,
155
155
  )
@@ -161,7 +161,7 @@ class AcsMeetingModule : Module() {
161
161
  // AP). Samsung then assoc-rejects the glasses SoftAP (status 1025) and fires
162
162
  // onUnavailable. After that request dies the STA is idle — the same join from idle
163
163
  // is what succeeded at 17:43:20 after a failed switch.
164
- com.mentra.acsmeeting.trace.SoftApTrace.stage(
164
+ com.mentra.glassesmedia.trace.SoftApTrace.stage(
165
165
  "scoped_join_unavailable_retry",
166
166
  "ssid" to ssid,
167
167
  "settleMs" to ScopedSoftApNetwork.UNAVAILABLE_RETRY_SETTLE_MS,
@@ -61,26 +61,26 @@ import com.mentra.acsmeeting.audio.PcmBridge
61
61
  import com.mentra.acsmeeting.audio.PhoneMicCapturer
62
62
  import com.mentra.acsmeeting.audio.UplinkPacer
63
63
  import com.mentra.acsmeeting.audio.UplinkSender
64
- import com.mentra.acsmeeting.source.AcsInvestigation
65
- import com.mentra.acsmeeting.source.CloudflareWhepSource
66
- import com.mentra.acsmeeting.source.DecoderMode
67
- import com.mentra.acsmeeting.source.OutgoingRateArm
68
- import com.mentra.acsmeeting.source.PixelFormatArm
69
- import com.mentra.acsmeeting.source.GlassesMediaController
70
- import com.mentra.acsmeeting.network.ScopedSoftApNetwork
71
- import com.mentra.acsmeeting.source.GlassesMediaSourceFactory
72
- import com.mentra.acsmeeting.source.LocalWhipIngestSource
64
+ import com.mentra.glassesmedia.source.MediaDiagnostics
65
+ import com.mentra.glassesmedia.source.CloudflareWhepSource
66
+ import com.mentra.glassesmedia.source.DecoderMode
67
+ import com.mentra.glassesmedia.source.OutgoingRateArm
68
+ import com.mentra.glassesmedia.source.PixelFormatArm
69
+ import com.mentra.glassesmedia.source.GlassesMediaController
70
+ import com.mentra.glassesmedia.network.ScopedSoftApNetwork
71
+ import com.mentra.glassesmedia.source.GlassesMediaSourceFactory
72
+ import com.mentra.glassesmedia.source.LocalWhipIngestSource
73
73
  import com.mentra.acsmeeting.source.MeetingVideoSourceSpec
74
- import com.mentra.acsmeeting.source.SourceConfig
75
- import com.mentra.acsmeeting.source.SourceKind
76
- import com.mentra.acsmeeting.source.SourceState
77
- import com.mentra.acsmeeting.source.SyntheticI420Source
78
- import com.mentra.acsmeeting.source.TargetSize
79
- import com.mentra.acsmeeting.source.VideoSourceArm
80
- import com.mentra.acsmeeting.telemetry.AvSyncProbe
81
- import com.mentra.acsmeeting.telemetry.PipelineStats
82
- import com.mentra.acsmeeting.telemetry.PipelineTicker
83
- import com.mentra.acsmeeting.trace.SoftApTrace
74
+ import com.mentra.glassesmedia.source.SourceConfig
75
+ import com.mentra.glassesmedia.source.SourceKind
76
+ import com.mentra.glassesmedia.source.SourceState
77
+ import com.mentra.glassesmedia.source.SyntheticI420Source
78
+ import com.mentra.glassesmedia.source.TargetSize
79
+ import com.mentra.glassesmedia.source.VideoSourceArm
80
+ import com.mentra.glassesmedia.telemetry.AvSyncProbe
81
+ import com.mentra.glassesmedia.telemetry.PipelineStats
82
+ import com.mentra.glassesmedia.telemetry.PipelineTicker
83
+ import com.mentra.glassesmedia.trace.SoftApTrace
84
84
  import com.mentra.acsmeeting.video.AcsFrameSender
85
85
  import com.mentra.acsmeeting.video.VideoProfile
86
86
  import java.util.concurrent.CountDownLatch
@@ -131,7 +131,7 @@ class AcsMeetingSession(
131
131
  private val resolvedFactory = mediaSourceFactory ?: GlassesMediaSourceFactory { video, pcm, config ->
132
132
  // The synthetic diagnostic arm overrides everything; otherwise the requested kind decides.
133
133
  when {
134
- AcsInvestigation.videoArm == VideoSourceArm.SYNTHETIC ->
134
+ MediaDiagnostics.videoArm == VideoSourceArm.SYNTHETIC ->
135
135
  SyntheticI420Source(video, stats, frameSender::isReady)
136
136
 
137
137
  config.kind == SourceKind.SOFTAP ->
@@ -207,7 +207,7 @@ class AcsMeetingSession(
207
207
  @Volatile private var phase = "idle"
208
208
  @Volatile private var lastError: String? = null
209
209
  @Volatile private var audioSource = "glasses"
210
- @Volatile private var configuredAudioDelayMs = AcsInvestigation.acsAudioDelayMs
210
+ @Volatile private var configuredAudioDelayMs = MediaDiagnostics.acsAudioDelayMs
211
211
  @Volatile private var lastSafety = AudioSafety.DEGRADED
212
212
  // Health of the glasses WHEP feed, reported alongside the ACS phase so the host
213
213
  // can tell "call is up, glasses video is dead" from a healthy call.
@@ -382,7 +382,7 @@ class AcsMeetingSession(
382
382
  // moves it runs on this executor, so the value is stable for the rest of this join.
383
383
  val generation = joinGeneration.get()
384
384
  val requested = video
385
- this.profile = when (AcsInvestigation.outgoingRate) {
385
+ this.profile = when (MediaDiagnostics.outgoingRate) {
386
386
  OutgoingRateArm.CLAMP_TO_SOFTWARE_CEILING -> requested.forSoftwareEncoder()
387
387
  OutgoingRateArm.ADVERTISE_REQUESTED -> requested
388
388
  }
@@ -396,7 +396,7 @@ class AcsMeetingSession(
396
396
  } else {
397
397
  Log.i(
398
398
  TAG,
399
- "ACS rate arm=${AcsInvestigation.outgoingRate} advertising " +
399
+ "ACS rate arm=${MediaDiagnostics.outgoingRate} advertising " +
400
400
  "${this.profile.width}x${this.profile.height}@${this.profile.fps} unclamped; " +
401
401
  "P7 rate names what binds",
402
402
  )
@@ -410,7 +410,7 @@ class AcsMeetingSession(
410
410
  emit("connecting")
411
411
  val bridge = PcmBridge(context.cacheDir, dumpWav)
412
412
  pcmBridge = bridge
413
- val delayMs = (audioDelayMs ?: AcsInvestigation.acsAudioDelayMs)
413
+ val delayMs = (audioDelayMs ?: MediaDiagnostics.acsAudioDelayMs)
414
414
  .coerceIn(0, AudioUplinkChain.MAX_DELAY_MS)
415
415
  configuredAudioDelayMs = delayMs
416
416
  val headroom = if (videoSource.kind == SourceKind.SOFTAP && parsed != AudioSourceKind.PHONE) {
@@ -458,7 +458,7 @@ class AcsMeetingSession(
458
458
  applyAudioPolicy("virtual-stream-state")
459
459
  }
460
460
 
461
- val synthetic = AcsInvestigation.videoArm == VideoSourceArm.SYNTHETIC
461
+ val synthetic = MediaDiagnostics.videoArm == VideoSourceArm.SYNTHETIC
462
462
  if (synthetic) muted.set(true)
463
463
  val desired = desiredKind()
464
464
  val plan = if (synthetic) {
@@ -489,7 +489,7 @@ class AcsMeetingSession(
489
489
  Log.i(TAG, "raw incoming audio state=${incoming.state}")
490
490
  }
491
491
  incoming.addOnMixedAudioBufferReceivedListener { event: IncomingMixedAudioEvent ->
492
- if (AcsInvestigation.videoArm == VideoSourceArm.SYNTHETIC) return@addOnMixedAudioBufferReceivedListener
492
+ if (MediaDiagnostics.videoArm == VideoSourceArm.SYNTHETIC) return@addOnMixedAudioBufferReceivedListener
493
493
  try {
494
494
  val data = event.audioBuffer?.buffer ?: return@addOnMixedAudioBufferReceivedListener
495
495
  val bytes = ByteArray(data.remaining())
@@ -535,14 +535,14 @@ class AcsMeetingSession(
535
535
  videoSource is MeetingVideoSourceSpec.SoftAp -> "softap"
536
536
  else -> "whep"
537
537
  }
538
- stats.pathMode = if (AcsInvestigation.decoderMode == DecoderMode.BYTE_BUFFER) "bytebuf" else "texture"
538
+ stats.pathMode = if (MediaDiagnostics.decoderMode == DecoderMode.BYTE_BUFFER) "bytebuf" else "texture"
539
539
  stats.pathCopy = when {
540
- AcsInvestigation.pixelFormat == PixelFormatArm.NV12 -> "nv12"
541
- AcsInvestigation.zeroCopy -> "zerocopy"
540
+ MediaDiagnostics.pixelFormat == PixelFormatArm.NV12 -> "nv12"
541
+ MediaDiagnostics.zeroCopy -> "zerocopy"
542
542
  else -> "planes"
543
543
  }
544
- stats.pix = AcsInvestigation.pixelFormat.name.lowercase()
545
- stats.zcOn = if (AcsInvestigation.zeroCopy) 1 else 0
544
+ stats.pix = MediaDiagnostics.pixelFormat.name.lowercase()
545
+ stats.zcOn = if (MediaDiagnostics.zeroCopy) 1 else 0
546
546
  mediaRestartAttempts = 0
547
547
  currentSourceKind = if (synthetic) SourceKind.DIRECT else videoSource.kind
548
548
  media.setStateListener { state, reason ->
@@ -599,12 +599,12 @@ class AcsMeetingSession(
599
599
  applyAudioPolicy("join")
600
600
  Log.i(
601
601
  TAG,
602
- "ACS join started arm=${AcsInvestigation.videoArm.name.lowercase()} " +
602
+ "ACS join started arm=${MediaDiagnostics.videoArm.name.lowercase()} " +
603
603
  "profile=${profile.width}x${profile.height}@${profile.fps} " +
604
604
  "maxBitrate=${profile.maxBitrateBps} bitsPerFrame=${profile.bitsPerFrame()} " +
605
- "syntheticFps=${AcsInvestigation.syntheticFps} entropy=${AcsInvestigation.syntheticEntropy} " +
606
- "decoderMode=${AcsInvestigation.decoderMode} zeroCopy=${AcsInvestigation.zeroCopy} " +
607
- "pixelFormat=${AcsInvestigation.pixelFormat} " +
605
+ "syntheticFps=${MediaDiagnostics.syntheticFps} entropy=${MediaDiagnostics.syntheticEntropy} " +
606
+ "decoderMode=${MediaDiagnostics.decoderMode} zeroCopy=${MediaDiagnostics.zeroCopy} " +
607
+ "pixelFormat=${MediaDiagnostics.pixelFormat} " +
608
608
  "source=${this.audioSource} audio=${if (synthetic) "off" else "on"} " +
609
609
  "armVirtual=${plan.armVirtual} transportMuted=${plan.transportMuted}",
610
610
  )
@@ -748,7 +748,7 @@ class AcsMeetingSession(
748
748
  }
749
749
 
750
750
  fun setMuted(next: Boolean): Map<String, Any> {
751
- if (AcsInvestigation.videoArm == VideoSourceArm.SYNTHETIC) {
751
+ if (MediaDiagnostics.videoArm == VideoSourceArm.SYNTHETIC) {
752
752
  muted.set(true)
753
753
  return snapshot()
754
754
  }
@@ -988,7 +988,7 @@ class AcsMeetingSession(
988
988
  Log.i(
989
989
  TAG,
990
990
  "P8 audio-up config configuredDelayMs=$configuredAudioDelayMs " +
991
- "audioTimestamps=${AcsInvestigation.acsAudioTimestamps}",
991
+ "audioTimestamps=${MediaDiagnostics.acsAudioTimestamps}",
992
992
  )
993
993
  }
994
994
 
@@ -3,8 +3,8 @@ package com.mentra.acsmeeting.audio
3
3
  import android.util.Log
4
4
  import com.azure.android.communication.calling.RawAudioBuffer
5
5
  import com.azure.android.communication.calling.RawOutgoingAudioStream
6
- import com.mentra.acsmeeting.source.AcsInvestigation
7
- import com.mentra.acsmeeting.telemetry.RingPercentile
6
+ import com.mentra.glassesmedia.source.MediaDiagnostics
7
+ import com.mentra.glassesmedia.telemetry.RingPercentile
8
8
  import com.mentra.acsmeeting.video.AcsTimestamp
9
9
  import java.nio.ByteBuffer
10
10
  import java.util.concurrent.atomic.AtomicBoolean
@@ -36,13 +36,13 @@ fun interface UplinkTransport {
36
36
  * Whether ACS actually *uses* these ticks to lip-sync raw audio at the receiver is an open question
37
37
  * that only a receiver-side recording answers. Stamping them is the precondition for asking it, not
38
38
  * the answer, and it is separately worth doing: a run of zero timestamps is what makes the Teams
39
- * jitter buffer hold. [AcsInvestigation.acsAudioTimestamps] turns it off so the question can be
39
+ * jitter buffer hold. [MediaDiagnostics.acsAudioTimestamps] turns it off so the question can be
40
40
  * asked both ways on the same device.
41
41
  */
42
42
  class AcsUplinkTransport(
43
43
  private val stream: RawOutgoingAudioStream,
44
44
  private val clock: () -> Long = System::nanoTime,
45
- private val stamp: Boolean = AcsInvestigation.acsAudioTimestamps,
45
+ private val stamp: Boolean = MediaDiagnostics.acsAudioTimestamps,
46
46
  ) : UplinkTransport {
47
47
  private var lastTicks = 0L
48
48
 
@@ -1,5 +1,8 @@
1
1
  package com.mentra.acsmeeting.source
2
2
 
3
+ import com.mentra.glassesmedia.source.SourceConfig
4
+ import com.mentra.glassesmedia.source.SourceKind
5
+
3
6
  /**
4
7
  * Where the meeting's glasses video comes from, as sent by the host.
5
8
  *
@@ -1,5 +1,7 @@
1
1
  package com.mentra.acsmeeting.video
2
2
 
3
+ import com.mentra.glassesmedia.video.I420Packer
4
+
3
5
  import android.util.Log
4
6
  import com.azure.android.communication.calling.RawOutgoingVideoStream
5
7
  import com.azure.android.communication.calling.RawVideoFrameBuffer
@@ -10,13 +12,13 @@ import com.azure.android.communication.calling.VideoStreamResolution
10
12
  import com.azure.android.communication.calling.VideoStreamState
11
13
  import com.azure.android.communication.calling.VideoStreamStateChangedListener
12
14
  import com.azure.android.communication.calling.VirtualOutgoingVideoStream
13
- import com.mentra.acsmeeting.source.AcsInvestigation
14
- import com.mentra.acsmeeting.source.I420Planes
15
- import com.mentra.acsmeeting.source.PixelFormatArm
16
- import com.mentra.acsmeeting.source.TargetSize
17
- import com.mentra.acsmeeting.telemetry.AvSyncProbe
18
- import com.mentra.acsmeeting.telemetry.ChromaProbe
19
- import com.mentra.acsmeeting.telemetry.PipelineStats
15
+ import com.mentra.glassesmedia.source.MediaDiagnostics
16
+ import com.mentra.glassesmedia.source.I420Planes
17
+ import com.mentra.glassesmedia.source.PixelFormatArm
18
+ import com.mentra.glassesmedia.source.TargetSize
19
+ import com.mentra.glassesmedia.telemetry.AvSyncProbe
20
+ import com.mentra.glassesmedia.telemetry.ChromaProbe
21
+ import com.mentra.glassesmedia.telemetry.PipelineStats
20
22
  import java.nio.ByteBuffer
21
23
  import java.util.concurrent.ConcurrentHashMap
22
24
  import java.util.concurrent.ConcurrentLinkedQueue
@@ -297,12 +299,12 @@ class AcsFrameSender(
297
299
  return try {
298
300
  negotiated.pixelFormat == VideoStreamPixelFormat.NV12
299
301
  } catch (_: Exception) {
300
- AcsInvestigation.pixelFormat == PixelFormatArm.NV12
302
+ MediaDiagnostics.pixelFormat == PixelFormatArm.NV12
301
303
  }
302
304
  }
303
305
 
304
306
  private fun tryZeroCopy(planes: I420Planes): Boolean {
305
- if (!AcsInvestigation.zeroCopy) return false
307
+ if (!MediaDiagnostics.zeroCopy) return false
306
308
  if (planes.retain == null || planes.release == null) {
307
309
  stats.onZcFell()
308
310
  return false
@@ -407,7 +409,7 @@ class AcsFrameSender(
407
409
  private const val ZC_GRACE_MS = 1_000L
408
410
 
409
411
  fun outgoingFormat(profile: VideoProfile = VideoProfile.DEFAULT): VideoStreamFormat =
410
- when (AcsInvestigation.pixelFormat) {
412
+ when (MediaDiagnostics.pixelFormat) {
411
413
  PixelFormatArm.NV12 -> nv12Format(profile)
412
414
  PixelFormatArm.I420 -> i420Format(profile)
413
415
  }
@@ -1,5 +1,7 @@
1
1
  package com.mentra.acsmeeting.video
2
2
 
3
+ import com.mentra.glassesmedia.video.I420Packer
4
+
3
5
  /**
4
6
  * ACS Android [com.azure.android.communication.calling.VideoStreamResolution]
5
7
  * names. The virtual outgoing stream wants these exact sizes. Near-matches
@@ -1,5 +1,7 @@
1
1
  package com.mentra.acsmeeting.video
2
2
 
3
+ import com.mentra.glassesmedia.video.I420Packer
4
+
3
5
  import java.nio.ByteBuffer
4
6
 
5
7
  /**
@@ -1,28 +1,28 @@
1
- package com.mentra.acsmeeting.source
1
+ package com.mentra.glassesmedia.source
2
2
 
3
3
  import com.mentra.acsmeeting.audio.AudioUplinkChain
4
4
  import org.assertj.core.api.Assertions.assertThat
5
5
  import org.junit.Test
6
6
 
7
- class AcsInvestigationTest {
7
+ class MediaDiagnosticsTest {
8
8
  @Test
9
9
  fun videoArmDefaultsToWhep() {
10
- assertThat(AcsInvestigation.videoArm).isEqualTo(VideoSourceArm.WHEP)
10
+ assertThat(MediaDiagnostics.videoArm).isEqualTo(VideoSourceArm.WHEP)
11
11
  }
12
12
 
13
13
  @Test
14
14
  fun decoderModeDefaultsToTexture() {
15
- assertThat(AcsInvestigation.decoderMode).isEqualTo(DecoderMode.TEXTURE)
15
+ assertThat(MediaDiagnostics.decoderMode).isEqualTo(DecoderMode.TEXTURE)
16
16
  }
17
17
 
18
18
  @Test
19
19
  fun zeroCopyDefaultsToOff() {
20
- assertThat(AcsInvestigation.zeroCopy).isFalse()
20
+ assertThat(MediaDiagnostics.zeroCopy).isFalse()
21
21
  }
22
22
 
23
23
  @Test
24
24
  fun pixelFormatDefaultsToI420() {
25
- assertThat(AcsInvestigation.pixelFormat).isEqualTo(PixelFormatArm.I420)
25
+ assertThat(MediaDiagnostics.pixelFormat).isEqualTo(PixelFormatArm.I420)
26
26
  }
27
27
 
28
28
  /**
@@ -34,7 +34,7 @@ class AcsInvestigationTest {
34
34
  */
35
35
  @Test
36
36
  fun outgoingRateAdvertisesWhatWasRequested() {
37
- assertThat(AcsInvestigation.outgoingRate).isEqualTo(OutgoingRateArm.ADVERTISE_REQUESTED)
37
+ assertThat(MediaDiagnostics.outgoingRate).isEqualTo(OutgoingRateArm.ADVERTISE_REQUESTED)
38
38
  }
39
39
 
40
40
  /**
@@ -46,12 +46,12 @@ class AcsInvestigationTest {
46
46
  */
47
47
  @Test
48
48
  fun audioDelayShipsAtZeroUntilItIsMeasured() {
49
- assertThat(AcsInvestigation.acsAudioDelayMs).isEqualTo(0)
49
+ assertThat(MediaDiagnostics.acsAudioDelayMs).isEqualTo(0)
50
50
  }
51
51
 
52
52
  @Test
53
53
  fun theConfiguredDelayStaysInsideTheChainsCeiling() {
54
- assertThat(AcsInvestigation.acsAudioDelayMs).isBetween(0, AudioUplinkChain.MAX_DELAY_MS)
54
+ assertThat(MediaDiagnostics.acsAudioDelayMs).isBetween(0, AudioUplinkChain.MAX_DELAY_MS)
55
55
  }
56
56
 
57
57
  /**
@@ -61,6 +61,6 @@ class AcsInvestigationTest {
61
61
  */
62
62
  @Test
63
63
  fun timestampsDefaultOnAndRemainSwitchable() {
64
- assertThat(AcsInvestigation.acsAudioTimestamps).isTrue()
64
+ assertThat(MediaDiagnostics.acsAudioTimestamps).isTrue()
65
65
  }
66
66
  }
@@ -1,5 +1,8 @@
1
1
  package com.mentra.acsmeeting.source
2
2
 
3
+ import com.mentra.glassesmedia.source.SourceConfig
4
+ import com.mentra.glassesmedia.source.SourceKind
5
+
3
6
  import org.assertj.core.api.Assertions.assertThat
4
7
  import org.assertj.core.api.Assertions.assertThatThrownBy
5
8
  import org.junit.Test
@@ -1,6 +1,6 @@
1
- package com.mentra.acsmeeting.telemetry
1
+ package com.mentra.glassesmedia.telemetry
2
2
 
3
- import com.mentra.acsmeeting.telemetry.VideoQuality.Band
3
+ import com.mentra.glassesmedia.telemetry.VideoQuality.Band
4
4
  import com.mentra.acsmeeting.video.VideoProfile
5
5
  import org.assertj.core.api.Assertions.assertThat
6
6
  import org.assertj.core.api.Assertions.within
@@ -1,5 +1,7 @@
1
1
  package com.mentra.acsmeeting.video
2
2
 
3
+ import com.mentra.glassesmedia.video.I420Packer
4
+
3
5
  import org.assertj.core.api.Assertions.assertThat
4
6
  import org.junit.Test
5
7
 
@@ -1,6 +1,6 @@
1
1
  package com.mentra.acsmeeting.video
2
2
 
3
- import com.mentra.acsmeeting.source.AcsInvestigation
3
+ import com.mentra.glassesmedia.source.MediaDiagnostics
4
4
  import org.assertj.core.api.Assertions.assertThat
5
5
  import org.junit.Test
6
6
 
@@ -34,7 +34,7 @@ class VideoProfileTest {
34
34
  */
35
35
  @Test
36
36
  fun declaredRateMatchesTheSyntheticEmitRate() {
37
- assertThat(VideoProfile.DEFAULT.fps).isEqualTo(AcsInvestigation.syntheticFps)
37
+ assertThat(VideoProfile.DEFAULT.fps).isEqualTo(MediaDiagnostics.syntheticFps)
38
38
  }
39
39
 
40
40
  /**
@@ -26,6 +26,8 @@ export type AcsMeetingState = {
26
26
  activeStream?: AcsActiveStream;
27
27
  audioSafety?: AcsAudioSafety;
28
28
  mediaSource?: AcsMediaSourceState;
29
+ /** Local WHIP endpoint, available after a SoftAP join. */
30
+ ingestUrl?: string;
29
31
  /** Remote roster (Android emits this; iOS does not yet). */
30
32
  participants?: AcsMeetingParticipant[];
31
33
  };
@@ -38,7 +40,14 @@ export type AcsOutgoingVideo = {
38
40
  export type AcsMeetingJoinOptions = {
39
41
  meetingUrl: string;
40
42
  token: string;
41
- whepUrl: string;
43
+ whepUrl?: string;
44
+ videoSource?: {
45
+ type: "whep";
46
+ url: string;
47
+ } | {
48
+ type: "softap";
49
+ bindAddress: string;
50
+ };
42
51
  displayName?: string;
43
52
  /** "glasses" sends WHEP PCM. "phone" uses the ACS local mic (handset or BT). */
44
53
  audioSource?: AcsAudioSource;
@@ -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,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,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,yDAAyD;IACzD,KAAK,CAAC,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACzC,aAAa,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAClD,CAAA"}
1
+ {"version":3,"file":"AcsMeeting.types.d.ts","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAA;AAEnG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,CAAA;AAChD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAC1D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAC3D;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE3E,MAAM,MAAM,0BAA0B,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,CAAA;AAEhH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,0BAA0B,CAAA;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,CAAA;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,0DAA0D;IAC1D,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 /** 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 displayName?: string\n /** \"glasses\" sends WHEP PCM. \"phone\" uses the ACS local mic (handset or BT). */\n audioSource?: AcsAudioSource\n /**\n * Hold outgoing PCM this many ms before ACS. SoftAP+LC3 sets this in the host\n * because BLE audio reaches the phone before SoftAP video.\n */\n audioDelayMs?: number\n /** Dump WHEP PCM to a WAV in cache for P4 verification. */\n dumpPcmWav?: boolean\n /** When omitted, native keeps 1280×720@15 / 2.5 Mbps. */\n video?: AcsOutgoingVideo\n}\n\nexport type AcsIncomingPcmEvent = {\n base64: string\n sampleRate: number\n channels: number\n}\n\nexport type AcsMeetingModuleEvents = {\n onState: (state: AcsMeetingState) => void\n onIncomingPcm: (pcm: AcsIncomingPcmEvent) => void\n}\n"]}
1
+ {"version":3,"file":"AcsMeeting.types.js","sourceRoot":"","sources":["../src/AcsMeeting.types.ts"],"names":[],"mappings":"","sourcesContent":["export type MeetingPhase = \"idle\" | \"connecting\" | \"lobby\" | \"connected\" | \"disconnected\" | \"error\"\n\nexport type AcsAudioSource = \"glasses\" | \"phone\"\nexport type AcsActiveStream = \"none\" | \"virtual\" | \"local\"\nexport type AcsAudioSafety = \"safe\" | \"degraded\" | \"unsafe\"\n/**\n * Health of the glasses WHEP subscription feeding the call. `failed` = ICE dropped or\n * the WHEP endpoint went away while the ACS call may still be `connected`. Native\n * rebuilds it on its own with backoff; the host can force one via `restartVideoSource`.\n */\nexport type AcsMediaSourceState = \"idle\" | \"connecting\" | \"live\" | \"failed\"\n\nexport type AcsMeetingParticipantState = \"idle\" | \"connecting\" | \"connected\" | \"lobby\" | \"hold\" | \"disconnected\"\n\nexport type AcsMeetingParticipant = {\n id: string\n displayName: string | null\n state: AcsMeetingParticipantState\n isMuted: boolean\n isSpeaking: boolean\n}\n\nexport type AcsMeetingState = {\n state: MeetingPhase\n muted: boolean\n error?: string\n meetingUrl?: string\n provider?: \"acs-teams\"\n audioSource?: AcsAudioSource\n activeStream?: AcsActiveStream\n audioSafety?: AcsAudioSafety\n mediaSource?: AcsMediaSourceState\n /** Local WHIP endpoint, available after a SoftAP join. */\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"]}
@@ -10,7 +10,7 @@ declare class AcsMeetingNativeModule extends NativeModule<AcsMeetingModuleEvents
10
10
  leave(): Promise<void>;
11
11
  /**
12
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.
13
+ * finished. Use this explicit barrier across platforms; Android `leave()` only queues cleanup.
14
14
  */
15
15
  leaveAndAwait(options: {
16
16
  timeoutMs: number;
@@ -22,7 +22,7 @@ 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
- /** SoftAP: join the glasses hotspot as a scoped network; resolves to the phone's IPv4 on it. Android only. */
25
+ /** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */
26
26
  joinScopedNetwork(ssid: string, passphrase: string): Promise<string>;
27
27
  beginTrace(traceId: string): Promise<void>;
28
28
  leaveScopedNetwork(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"AcsMeetingModule.d.ts","sourceRoot":"","sources":["../src/AcsMeetingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,KAAK,EAAC,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,oBAAoB,CAAA;AAEtG,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IAC/E,IAAI,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAC9D,iFAAiF;IACjF,YAAY,CAAC,OAAO,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,eAAe,CAAC;IACtF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB;;;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
+ {"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,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;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
+ {"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. 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 /** SoftAP: join the glasses hotspot; resolves to the phone's IPv4 on it. */\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"]}
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
18
18
  s.dependency 'AzureCommunicationCalling', '~> 2.15'
19
19
  s.dependency 'AzureCommunicationCommon', '~> 1.1'
20
20
  # Pin to the same WebRTC-SDK the Mentra App already links via LiveKit.
21
- s.dependency 'WebRTC-SDK', '137.7151.09'
21
+ s.dependency 'GlassesMedia'
22
22
  s.frameworks = 'AVFoundation', 'AudioToolbox', 'CoreMedia', 'CoreVideo', 'UIKit'
23
23
  s.pod_target_xcconfig = {
24
24
  'DEFINES_MODULE' => 'YES',