@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
@@ -1,155 +0,0 @@
1
- package com.mentra.acsmeeting.telemetry
2
-
3
- import android.util.Log
4
- import kotlin.math.abs
5
-
6
- /**
7
- * Measures how far glasses audio leads glasses video at ACS ingest.
8
- *
9
- * SoftAP video and BLE LC3 do not share a capture clock, so first-frame arrival is startup
10
- * skew, not lip-sync. This probe watches both paths on [System.nanoTime]: a clap in frame is a
11
- * loud PCM transient and a luma jump. Their difference is [lastAudioLeadMs] — the number the
12
- * delay line can cancel. ACS/Teams jitter after ingest is still a receiver recording.
13
- *
14
- * Audio is sampled at uplink ingest, in front of any configured delay, so the lead is the raw
15
- * path offset even when the host already holds PCM for alignment.
16
- */
17
- class AvSyncProbe(
18
- private val clock: () -> Long = System::nanoTime,
19
- private val log: (String) -> Unit = { Log.i(TAG, it) },
20
- ) {
21
- private val lock = Any()
22
- private var lastMeanAbs = -1
23
- private var lastLumaY = -1
24
- private var pendingAudioNs = 0L
25
- private var pendingVideoNs = 0L
26
- private var pendingMeanAbs = -1
27
- private var pendingLumaY = -1
28
- private var cooldownUntilNs = 0L
29
- @Volatile private var clapCount = 0
30
- @Volatile private var lastAudioLeadMs: Int? = null
31
- @Volatile private var publishedMeanAbs = -1
32
- @Volatile private var publishedLumaY = -1
33
-
34
- fun onAudio(pcm: ByteArray, nowNs: Long = clock()) {
35
- onAudioLevel(meanAbs16(pcm), nowNs)
36
- }
37
-
38
- fun onAudioLevel(meanAbs: Int, nowNs: Long = clock()) {
39
- publishedMeanAbs = meanAbs
40
- synchronized(lock) {
41
- val prev = lastMeanAbs
42
- lastMeanAbs = meanAbs
43
- expire(nowNs)
44
- if (nowNs < cooldownUntilNs) return
45
- if (!isAudioTransient(prev, meanAbs)) return
46
- if (pendingVideoNs > 0L) {
47
- emit(pendingVideoNs - nowNs, meanAbs, pendingLumaY, nowNs)
48
- return
49
- }
50
- pendingAudioNs = nowNs
51
- pendingMeanAbs = meanAbs
52
- }
53
- }
54
-
55
- fun onVideoLuma(y: Int, nowNs: Long = clock()) {
56
- publishedLumaY = y
57
- synchronized(lock) {
58
- val prev = lastLumaY
59
- lastLumaY = y
60
- expire(nowNs)
61
- if (nowNs < cooldownUntilNs) return
62
- if (!isVideoTransient(prev, y)) return
63
- if (pendingAudioNs > 0L) {
64
- emit(nowNs - pendingAudioNs, pendingMeanAbs, y, nowNs)
65
- return
66
- }
67
- pendingVideoNs = nowNs
68
- pendingLumaY = y
69
- }
70
- }
71
-
72
- fun lastAudioLeadMs(): Int? = lastAudioLeadMs
73
-
74
- fun clapCount(): Int = clapCount
75
-
76
- fun tick(videoE2eP50: String, videoAgeP50: String): String {
77
- val lead = lastAudioLeadMs
78
- return "AVSYNC live videoE2eP50=$videoE2eP50 " +
79
- "videoAgeP50=$videoAgeP50 " +
80
- "lastClapLeadMs=${lead?.toString() ?: "na"} clapN=$clapCount " +
81
- "meanAbs=$publishedMeanAbs lumaY=$publishedLumaY"
82
- }
83
-
84
- fun reset() {
85
- synchronized(lock) {
86
- lastMeanAbs = -1
87
- lastLumaY = -1
88
- pendingAudioNs = 0L
89
- pendingVideoNs = 0L
90
- pendingMeanAbs = -1
91
- pendingLumaY = -1
92
- cooldownUntilNs = 0L
93
- clapCount = 0
94
- lastAudioLeadMs = null
95
- publishedMeanAbs = -1
96
- publishedLumaY = -1
97
- }
98
- }
99
-
100
- private fun emit(leadNs: Long, meanAbs: Int, lumaY: Int, nowNs: Long) {
101
- val leadMs = (leadNs / 1_000_000L).toInt()
102
- lastAudioLeadMs = leadMs
103
- clapCount += 1
104
- pendingAudioNs = 0L
105
- pendingVideoNs = 0L
106
- pendingMeanAbs = -1
107
- pendingLumaY = -1
108
- cooldownUntilNs = nowNs + COOLDOWN_NS
109
- log("AVSYNC clap audioLeadMs=$leadMs meanAbs=$meanAbs lumaY=$lumaY clapN=$clapCount")
110
- }
111
-
112
- private fun expire(nowNs: Long) {
113
- if (pendingAudioNs > 0L && nowNs - pendingAudioNs > MATCH_WINDOW_NS) {
114
- pendingAudioNs = 0L
115
- pendingMeanAbs = -1
116
- }
117
- if (pendingVideoNs > 0L && nowNs - pendingVideoNs > MATCH_WINDOW_NS) {
118
- pendingVideoNs = 0L
119
- pendingLumaY = -1
120
- }
121
- }
122
-
123
- companion object {
124
- private const val TAG = "ACS-SPIKE"
125
- internal const val AUDIO_CLAP = 2_000
126
- internal const val AUDIO_RISE = 1_500
127
- internal const val VIDEO_JUMP = 10
128
- internal const val MATCH_WINDOW_NS = 500L * 1_000_000L
129
- internal const val COOLDOWN_NS = 750L * 1_000_000L
130
-
131
- fun meanAbs16(pcm: ByteArray): Int {
132
- var acc = 0L
133
- var n = 0
134
- var i = 0
135
- while (i + 1 < pcm.size) {
136
- val s = (pcm[i].toInt() and 0xff) or (pcm[i + 1].toInt() shl 8)
137
- val signed = if (s >= 0x8000) s - 0x10000 else s
138
- acc += abs(signed)
139
- n++
140
- i += 2
141
- }
142
- return if (n == 0) 0 else (acc / n).toInt()
143
- }
144
-
145
- internal fun isAudioTransient(previous: Int, current: Int): Boolean {
146
- if (previous < 0 || current < AUDIO_CLAP) return false
147
- return current - previous >= AUDIO_RISE
148
- }
149
-
150
- internal fun isVideoTransient(previous: Int, current: Int): Boolean {
151
- if (previous < 0) return false
152
- return abs(current - previous) >= VIDEO_JUMP
153
- }
154
- }
155
- }
@@ -1,78 +0,0 @@
1
- package com.mentra.acsmeeting.telemetry
2
-
3
- import com.mentra.acsmeeting.video.I420Packer
4
- import java.nio.ByteBuffer
5
- import kotlin.math.max
6
-
7
- /** 64-sample plane averages. Neutral content sits near u≈v≈128. */
8
- object ChromaProbe {
9
- const val SAMPLES = 64
10
-
11
- data class Sample(val y: Int, val u: Int, val v: Int)
12
-
13
- fun sampleNv12(y: ByteBuffer, uv: ByteBuffer): Sample {
14
- val yView = y.duplicate()
15
- val uvView = uv.duplicate()
16
- val origin = uvView.position()
17
- val size = uvView.remaining()
18
- return Sample(
19
- y = average(yView, yView.position(), yView.remaining()),
20
- u = averageEveryOther(uvView, origin, size, even = true),
21
- v = averageEveryOther(uvView, origin, size, even = false),
22
- )
23
- }
24
-
25
- fun samplePlanes(y: ByteBuffer, u: ByteBuffer, v: ByteBuffer): Sample {
26
- val yView = y.duplicate()
27
- val uView = u.duplicate()
28
- val vView = v.duplicate()
29
- return Sample(
30
- y = average(yView, yView.position(), yView.remaining()),
31
- u = average(uView, uView.position(), uView.remaining()),
32
- v = average(vView, vView.position(), vView.remaining()),
33
- )
34
- }
35
-
36
- fun samplePacked(src: ByteBuffer, width: Int, height: Int): Sample {
37
- val ySize = width * height
38
- val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
39
- val view = src.duplicate()
40
- return Sample(
41
- y = average(view, 0, ySize),
42
- u = average(view, ySize, uvSize),
43
- v = average(view, ySize + uvSize, uvSize),
44
- )
45
- }
46
-
47
- fun average(buffer: ByteBuffer, offset: Int, size: Int): Int {
48
- if (size <= 0) return 0
49
- val step = max(1, size / SAMPLES)
50
- var sum = 0L
51
- var count = 0
52
- var i = offset
53
- val end = offset + size
54
- while (i < end && count < SAMPLES) {
55
- sum += buffer.get(i).toInt() and 0xFF
56
- i += step
57
- count += 1
58
- }
59
- return if (count == 0) 0 else (sum / count).toInt()
60
- }
61
-
62
- private fun averageEveryOther(buffer: ByteBuffer, offset: Int, size: Int, even: Boolean): Int {
63
- if (size <= 1) return 0
64
- val start = offset + if (even) 0 else 1
65
- val raw = max(2, size / SAMPLES)
66
- val step = raw + (raw and 1)
67
- var sum = 0L
68
- var count = 0
69
- var i = start
70
- val end = offset + size
71
- while (i < end && count < SAMPLES) {
72
- sum += buffer.get(i).toInt() and 0xFF
73
- i += step
74
- count += 1
75
- }
76
- return if (count == 0) 0 else (sum / count).toInt()
77
- }
78
- }
@@ -1,372 +0,0 @@
1
- package com.mentra.acsmeeting.telemetry
2
-
3
- import com.mentra.acsmeeting.video.I420Packer
4
- import java.util.concurrent.atomic.AtomicInteger
5
- import java.util.concurrent.atomic.AtomicLong
6
- import kotlin.math.roundToInt
7
-
8
- /**
9
- * Monotonic pipeline counters. The 1 Hz [tick] prints rates for eyes; [cum]
10
- * is what pass/fail uses.
11
- *
12
- * inFlight is derived (sink - sub - drop), never tracked. A tracked counter
13
- * cannot be read atomically alongside the others, so onSub's increment-then-
14
- * decrement window printed CONSERVE_FAIL on a healthy pipeline. Deriving it
15
- * makes conservation an identity; reading sink last keeps it non-negative.
16
- */
17
- class PipelineStats(
18
- private val nowMs: () -> Long = { System.currentTimeMillis() },
19
- ) {
20
- private val sink = AtomicInteger(0)
21
- private val sub = AtomicInteger(0)
22
- private val dropSize = AtomicInteger(0)
23
- private val dropBusy = AtomicInteger(0)
24
- private val dropNotStarted = AtomicInteger(0)
25
- private val dropFail = AtomicInteger(0)
26
- private val dropNullI420 = AtomicInteger(0)
27
- private val dropPaced = AtomicInteger(0)
28
- private val abandoned = AtomicInteger(0)
29
- private val dup = AtomicInteger(0)
30
- private val rot = AtomicInteger(0)
31
-
32
- private val destAlloc = AtomicInteger(0)
33
- private val planeAlloc = AtomicInteger(0)
34
-
35
- private val lastTickSink = AtomicInteger(0)
36
- private val lastTickSub = AtomicInteger(0)
37
- private val lastTickPackets = AtomicInteger(0)
38
- private val lastPacketsAtMs = AtomicLong(0)
39
- private val lastTickMs = AtomicLong(0)
40
-
41
- private val lastArrivalNs = AtomicLong(0)
42
-
43
- val gap = RingPercentile()
44
- val pack = RingPercentile()
45
- val scale = RingPercentile()
46
- val toI420 = RingPercentile()
47
- val sinkCb = RingPercentile()
48
- val split = RingPercentile()
49
- val copy = RingPercentile()
50
- val send = RingPercentile()
51
-
52
- /**
53
- * How stale a frame already is when the decoder hands it to us.
54
- *
55
- * Everything else on this ladder measures work we do, so all of it can read healthy while the
56
- * wearer's picture lags seconds behind: capture, encode, and the network happen before our first
57
- * timestamp. WebRTC's extrapolated capture time is the only upstream clock we get, and it is an
58
- * estimate — read the trend, not the absolute value.
59
- */
60
- val age = RingPercentile()
61
-
62
- /**
63
- * Wait between handing a frame to the ACS sender and its single send thread picking it up.
64
- *
65
- * The one stage with no other symptom. `sendP95` times the ACS call itself, so a backed-up
66
- * `acs-i420-send` queue shows only as drops at the gate, with every timing on this ladder still
67
- * looking fast.
68
- */
69
- val queue = RingPercentile()
70
-
71
- /** Decoder-estimated capture time to ACS accepting the frame: the number the far end feels. */
72
- val e2e = RingPercentile()
73
-
74
- /**
75
- * WebRTC inbound-rtp disposition. Cumulative; the ladder prints deltas so a
76
- * climbing counter is visible without reading absolute values.
77
- */
78
- data class RecvHealth(
79
- val assembled: Long = -1L,
80
- val dropped: Long = -1L,
81
- val packetsLost: Long = -1L,
82
- val nack: Long = -1L,
83
- val pli: Long = -1L,
84
- val freezes: Long = -1L,
85
- val freezeSec: Double = -1.0,
86
- val jitter: Double = -1.0,
87
- val decodeSec: Double = -1.0,
88
- val jitterBufferSec: Double = -1.0,
89
- val jitterBufferEmits: Long = -1L,
90
- val decImpl: String = "",
91
- )
92
-
93
- @Volatile var arm: String = "whep"
94
- /** texture | bytebuf — which decoder factory we built. */
95
- @Volatile var pathMode: String = "texture"
96
- /** packsplit | planes | zerocopy | nv12 — how pixels reach ACS. */
97
- @Volatile var pathCopy: String = "packsplit"
98
- /** i420 | nv12 — pixel format advertised to ACS. */
99
- @Volatile var pix: String = "i420"
100
- /** MEDIA_STATISTICS codecName, underscored. Empty until the first report. */
101
- @Volatile var codecName: String = ""
102
- /** The fps we declared to ACS, so [VideoRateVerdict] can score against it. */
103
- @Volatile var advertisedFps: Double = 0.0
104
- /** The bitrate ceiling we granted ACS, for the starved-versus-CPU-bound split. */
105
- @Volatile var budgetBps: Int = 0
106
- /** Last rates computed by [tick], kept so the verdict scores the same numbers the ladder printed. */
107
- @Volatile var lastSinkFps: Double = 0.0
108
- @Volatile var lastSubFps: Double = 0.0
109
- @Volatile var decodedFps: Double? = null
110
- @Volatile var recvFps: Double? = null
111
- @Volatile var wireFps: Double? = null
112
- @Volatile var wireWidth: Int? = null
113
- @Volatile var wireHeight: Int? = null
114
- @Volatile var wireBitrateBps: Long? = null
115
- /** ACS cumulative outgoing packet count; differenced per tick into [lastPacketsPerSecond]. */
116
- @Volatile var wirePacketCount: Int? = null
117
- @Volatile var lastPacketsPerSecond: Double? = null
118
-
119
- /**
120
- * Latest ACS `networkSendQuality`, the only send-side network signal the SDK
121
- * exposes. It arrives on change rather than on a schedule, so it is latched here
122
- * and reprinted every tick — a GOOD/POOR that fired once before the bitrate
123
- * started sliding is exactly the line nobody scrolls back far enough to find.
124
- */
125
- @Volatile var sendQuality: String = ""
126
- @Volatile private var recv: RecvHealth? = null
127
- @Volatile private var recvPrev: RecvHealth? = null
128
- @Volatile var width: Int = 0
129
- @Volatile var height: Int = 0
130
- @Volatile var chromaY: Int = 0
131
- @Volatile var chromaU: Int = 0
132
- @Volatile var chromaV: Int = 0
133
- @Volatile var strideY: Int = 0
134
- @Volatile var strideU: Int = 0
135
- @Volatile var strideV: Int = 0
136
- @Volatile var zcOn: Int = 0
137
-
138
- private val bufTex = AtomicInteger(0)
139
- private val bufI420 = AtomicInteger(0)
140
- private val bufOther = AtomicInteger(0)
141
- private val strideTight = AtomicInteger(0)
142
- private val stridePadded = AtomicInteger(0)
143
- private val zcUsed = AtomicInteger(0)
144
- private val zcFell = AtomicInteger(0)
145
- private val zcPadded = AtomicInteger(0)
146
- private val zcHeldMax = AtomicInteger(0)
147
- private val zcTimeout = AtomicInteger(0)
148
-
149
- fun onSink() {
150
- sink.incrementAndGet()
151
- }
152
-
153
- fun recordGap(nowNs: Long = System.nanoTime()) {
154
- val prev = lastArrivalNs.getAndSet(nowNs)
155
- if (prev != 0L) gap.record(nowNs - prev)
156
- }
157
-
158
- /** Kept as the explicit hand-off point; inFlight is derived, so this records nothing. */
159
- fun onQueued() = Unit
160
-
161
- fun onSub() {
162
- sub.incrementAndGet()
163
- }
164
-
165
- fun onDropSize() = dropSize.incrementAndGet()
166
- fun onDropBusy() = dropBusy.incrementAndGet()
167
- fun onDropNotStarted() = dropNotStarted.incrementAndGet()
168
- fun onDropFail() = dropFail.incrementAndGet()
169
- fun onDropMalformed() = dropFail.incrementAndGet()
170
-
171
- /**
172
- * Dropped because it arrived sooner than the rate we advertised to ACS.
173
- *
174
- * Distinct from [onDropBusy]: the send thread is idle, we just refuse to
175
- * over-feed the software encoder. Counted in [dropCount] so conservation
176
- * still holds after the sink has already seen the frame.
177
- */
178
- fun onDropPaced() = dropPaced.incrementAndGet()
179
-
180
- /**
181
- * A send that timed out. The future is not cancelled, so ACS may still read those
182
- * direct buffers; they are never recycled. Non-zero here means the pool is leaking.
183
- */
184
- fun onAbandoned() = abandoned.incrementAndGet()
185
-
186
- fun abandonedCount(): Int = abandoned.get()
187
- fun onDropNullI420() = dropNullI420.incrementAndGet()
188
- fun onDup() = dup.incrementAndGet()
189
- fun onRotation() = rot.incrementAndGet()
190
- fun onDestAlloc() = destAlloc.incrementAndGet()
191
- fun onPlaneAlloc() = planeAlloc.incrementAndGet()
192
- fun destAllocCount(): Int = destAlloc.get()
193
- fun planeAllocCount(): Int = planeAlloc.get()
194
-
195
- /**
196
- * Classify the WebRTC [VideoFrame.Buffer] without importing WebRTC here.
197
- * [kind] is "tex", "i420", or anything else ("other").
198
- */
199
- fun onFrameBuffer(kind: String) {
200
- when (kind) {
201
- "tex" -> bufTex.incrementAndGet()
202
- "i420" -> bufI420.incrementAndGet()
203
- else -> bufOther.incrementAndGet()
204
- }
205
- }
206
-
207
- fun onStrides(strideY: Int, strideU: Int, strideV: Int, width: Int) {
208
- this.strideY = strideY
209
- this.strideU = strideU
210
- this.strideV = strideV
211
- val chroma = I420Packer.chromaStride(width)
212
- if (strideY == width && strideU == chroma && strideV == chroma) {
213
- strideTight.incrementAndGet()
214
- } else {
215
- stridePadded.incrementAndGet()
216
- }
217
- }
218
-
219
- fun onZcUsed(heldNow: Int) {
220
- zcUsed.incrementAndGet()
221
- zcHeldMax.updateAndGet { current -> if (heldNow > current) heldNow else current }
222
- }
223
-
224
- fun onZcFell() = zcFell.incrementAndGet()
225
- fun onZcPadded() = zcPadded.incrementAndGet()
226
- fun onZcTimeout() = zcTimeout.incrementAndGet()
227
-
228
- fun zcUsedCount(): Int = zcUsed.get()
229
- fun zcFellCount(): Int = zcFell.get()
230
- fun zcTimeoutCount(): Int = zcTimeout.get()
231
- fun zcHeldMax(): Int = zcHeldMax.get()
232
- fun strideTightCount(): Int = strideTight.get()
233
- fun stridePaddedCount(): Int = stridePadded.get()
234
- fun bufTexCount(): Int = bufTex.get()
235
- fun bufI420Count(): Int = bufI420.get()
236
- fun bufOtherCount(): Int = bufOther.get()
237
-
238
- fun setSize(w: Int, h: Int) {
239
- width = w
240
- height = h
241
- }
242
-
243
- fun setRecvHealth(next: RecvHealth) {
244
- recvPrev = recv
245
- recv = next
246
- }
247
-
248
- /** Per-tick deltas plus the two absolutes that only mean something cumulatively. */
249
- fun recvLabel(): String {
250
- val now = recv ?: return "recv{na}"
251
- val was = recvPrev
252
- fun d(pick: (RecvHealth) -> Long): String {
253
- val current = pick(now)
254
- if (current < 0) return "na"
255
- val previous = was?.let(pick) ?: return current.toString()
256
- return if (previous < 0) current.toString() else (current - previous).toString()
257
- }
258
- val decodeMs = if (now.decodeSec < 0 || now.assembled <= 0) "na"
259
- else format(now.decodeSec * 1000.0 / now.assembled)
260
- val jbMs = if (now.jitterBufferSec < 0 || now.jitterBufferEmits <= 0) "na"
261
- else format(now.jitterBufferSec * 1000.0 / now.jitterBufferEmits)
262
- val impl = now.decImpl.ifBlank { "na" }.replace(' ', '_')
263
- return "recv{drop=${d { it.dropped }} lost=${d { it.packetsLost }} nack=${d { it.nack }} " +
264
- "pli=${d { it.pli }} freeze=${d { it.freezes }} freezeSec=${format(now.freezeSec)} " +
265
- "jit=${format(now.jitter * 1000.0)} decMs=$decodeMs jbMs=$jbMs decImpl=$impl}"
266
- }
267
-
268
- fun setChroma(sample: ChromaProbe.Sample) {
269
- chromaY = sample.y
270
- chromaU = sample.u
271
- chromaV = sample.v
272
- }
273
-
274
- fun sinkCount(): Int = sink.get()
275
- fun subCount(): Int = sub.get()
276
- fun dropCount(): Int =
277
- dropSize.get() + dropBusy.get() + dropNotStarted.get() + dropFail.get() + dropNullI420.get() + dropPaced.get()
278
-
279
- /** Read sink last: every frame hits sink before sub or drop, so this cannot go negative. */
280
- fun inFlightCount(): Int {
281
- val settled = sub.get() + dropCount()
282
- return sink.get() - settled
283
- }
284
-
285
- fun dupCount(): Int = dup.get()
286
- fun rotCount(): Int = rot.get()
287
-
288
- fun conserved(): Boolean = inFlightCount() >= 0
289
-
290
- /**
291
- * The phone-side stage with the highest p95, so a soak log names its own bottleneck.
292
- *
293
- * Only stages we actually perform are ranked. [age] is excluded on purpose: it is dominated by
294
- * capture, encode and the network, so including it would make every healthy call blame a stage
295
- * this device does not run. It is printed beside the verdict instead, which is what separates
296
- * "the phone is slow" from "the phone is fine and the frames arrive late".
297
- */
298
- fun slowestStage(): String {
299
- val stages = listOf(
300
- "scale" to scale.p95Ms(),
301
- "toI420" to toI420.p95Ms(),
302
- "copy" to copy.p95Ms(),
303
- "queue" to queue.p95Ms(),
304
- "send" to send.p95Ms(),
305
- )
306
- val worst = stages.filter { it.second >= 0 }.maxByOrNull { it.second } ?: return "na"
307
- return "${worst.first}=${format(worst.second)}"
308
- }
309
-
310
- fun tick(): String {
311
- val now = nowMs()
312
- val previous = lastTickMs.get()
313
- val dt = if (previous == 0L) 1000L else (now - previous).coerceAtLeast(1L)
314
- lastTickMs.set(now)
315
- val sinkNow = sink.get()
316
- val subNow = sub.get()
317
- val sinkDelta = sinkNow - lastTickSink.getAndSet(sinkNow)
318
- val subDelta = subNow - lastTickSub.getAndSet(subNow)
319
- val sinkRate = rate(sinkDelta, dt)
320
- val subRate = rate(subDelta, dt)
321
- lastSinkFps = sinkDelta * 1000.0 / dt
322
- lastSubFps = subDelta * 1000.0 / dt
323
- // ACS reports packets cumulatively on the MEDIA_STATISTICS interval, which is
324
- // several times longer than this 1 Hz tick. Differencing every tick reported
325
- // pps=0.0 on every tick between reports, so rate is measured across the gap
326
- // between actual changes and held in between rather than recomputed against a
327
- // count that did not move.
328
- wirePacketCount?.let { total ->
329
- val previous = lastTickPackets.get()
330
- if (total != previous) {
331
- val previousAt = lastPacketsAtMs.getAndSet(now)
332
- if (previous in 1..total && previousAt > 0) {
333
- lastPacketsPerSecond = (total - previous) * 1000.0 / (now - previousAt).coerceAtLeast(1L)
334
- }
335
- lastTickPackets.set(total)
336
- }
337
- }
338
- val dec = decodedFps?.let { formatRate(it) } ?: "na"
339
- val rcv = recvFps?.let { formatRate(it) } ?: "na"
340
- val wireFpsLabel = wireFps?.let { formatRate(it) } ?: "na"
341
- val wireW = wireWidth ?: 0
342
- val wireH = wireHeight ?: 0
343
- val wireKbps = wireBitrateBps?.let { (it / 1000).toString() } ?: "na"
344
- val codec = codecName.ifBlank { "na" }.replace(' ', '_')
345
- val wire = "${wireW}x${wireH}@$wireFpsLabel kbps=$wireKbps codec=$codec"
346
- val sizeLabel = if (width > 0 && height > 0) "${width}x${height}" else "0x0"
347
- val inFlightNow = inFlightCount()
348
- val conserve = if (inFlightNow >= 0) "" else " CONSERVE_FAIL"
349
- return "P6 ladder arm=$arm $sizeLabel recv=$rcv dec=$dec sink=$sinkRate dup=${dup.get()} sub=$subRate wire=$wire rot=${rot.get()} " +
350
- "drop{size=${dropSize.get()} busy=${dropBusy.get()} notStarted=${dropNotStarted.get()} fail=${dropFail.get()} nullI420=${dropNullI420.get()} pace=${dropPaced.get()} abandoned=${abandoned.get()}} " +
351
- "${recvLabel()} " +
352
- "path{mode=$pathMode copy=$pathCopy pix=$pix} " +
353
- "buf{tex=${bufTex.get()} i420=${bufI420.get()} other=${bufOther.get()}} " +
354
- "stride{y=$strideY u=$strideU v=$strideV tight=${strideTight.get()} padded=${stridePadded.get()}} " +
355
- "zc{on=$zcOn used=${zcUsed.get()} fell=${zcFell.get()} padded=${zcPadded.get()} heldMax=${zcHeldMax.get()} timeout=${zcTimeout.get()}} " +
356
- "ms{gapP50=${gap.p50()} gapP95=${gap.p95()} i420P95=${toI420.p95()} packP95=${pack.p95()} scaleP95=${scale.p95()} sinkCbP95=${sinkCb.p95()} splitP95=${split.p95()} copyP95=${copy.p95()} queueP95=${queue.p95()} sendP95=${send.p95()}} " +
357
- "lat{ageP50=${age.p50()} ageP95=${age.p95()} e2eP50=${e2e.p50()} e2eP95=${e2e.p95()} slowest=${slowestStage()}} " +
358
- "alloc{dest=${destAlloc.get()} plane=${planeAlloc.get()}} " +
359
- "chroma{y=$chromaY u=$chromaU v=$chromaV} " +
360
- "cum{sink=$sinkNow sub=$subNow drop=${dropCount()} inFlight=$inFlightNow}" +
361
- conserve
362
- }
363
-
364
- private fun rate(delta: Int, dtMs: Long): String = formatRate(delta * 1000.0 / dtMs)
365
-
366
- companion object {
367
- fun formatRate(value: Double): String = ((value * 10).roundToInt() / 10.0).toString()
368
-
369
- fun format(value: Double): String =
370
- if (value < 0) "na" else ((value * 10).roundToInt() / 10.0).toString()
371
- }
372
- }
@@ -1,79 +0,0 @@
1
- package com.mentra.acsmeeting.telemetry
2
-
3
- import android.os.Handler
4
- import android.os.Looper
5
- import android.os.Process
6
-
7
- /** Timer-driven 1 Hz emit so a stall prints sink=0 instead of going silent. */
8
- class PipelineTicker(
9
- private val stats: PipelineStats,
10
- private val avSync: AvSyncProbe? = null,
11
- looper: Looper = Looper.getMainLooper(),
12
- private val elapsedCpuMs: () -> Long = { Process.getElapsedCpuTime() },
13
- private val cores: Int = Runtime.getRuntime().availableProcessors(),
14
- private val emit: (String) -> Unit,
15
- ) {
16
- private val handler = Handler(looper)
17
- private var lastCpuMs = -1L
18
- private var lastWallMs = 0L
19
- private val tick = object : Runnable {
20
- override fun run() {
21
- // CPU is sampled once and shared: the ladder prints it and the verdict uses
22
- // it to tell an encoder that cannot keep up from one being throttled.
23
- val percent = samplePercent()
24
- emit("${stats.tick()} ${ProcessCpu.label(percent, cores)}")
25
- emit(
26
- VideoRateVerdict.line(
27
- advertisedFps = stats.advertisedFps,
28
- sinkFps = stats.lastSinkFps,
29
- admittedFps = stats.lastSubFps,
30
- wireFps = stats.wireFps,
31
- wireBitrateBps = stats.wireBitrateBps,
32
- budgetBps = stats.budgetBps,
33
- cpuPercent = percent,
34
- codecName = stats.codecName,
35
- ),
36
- )
37
- emit(
38
- VideoQuality.line(
39
- wireWidth = stats.wireWidth,
40
- wireHeight = stats.wireHeight,
41
- wireFps = stats.wireFps,
42
- wireBitrateBps = stats.wireBitrateBps,
43
- budgetBps = stats.budgetBps,
44
- packetsPerSecond = stats.lastPacketsPerSecond,
45
- cpuPercent = percent,
46
- sendQuality = stats.sendQuality,
47
- ),
48
- )
49
- avSync?.let { emit(it.tick(stats.e2e.p50(), stats.age.p50())) }
50
- handler.postDelayed(this, INTERVAL_MS)
51
- }
52
- }
53
-
54
- fun start() {
55
- lastCpuMs = -1L
56
- lastWallMs = 0L
57
- handler.removeCallbacks(tick)
58
- handler.post(tick)
59
- }
60
-
61
- fun stop() {
62
- handler.removeCallbacks(tick)
63
- }
64
-
65
- private fun samplePercent(): Double? {
66
- val now = System.currentTimeMillis()
67
- val cpu = elapsedCpuMs()
68
- val prevCpu = lastCpuMs
69
- val prevWall = lastWallMs
70
- lastCpuMs = cpu
71
- lastWallMs = now
72
- if (prevCpu < 0 || prevWall <= 0) return null
73
- return ProcessCpu.percent(cpu - prevCpu, now - prevWall)
74
- }
75
-
76
- companion object {
77
- const val INTERVAL_MS = 1000L
78
- }
79
- }
@@ -1,15 +0,0 @@
1
- package com.mentra.acsmeeting.telemetry
2
-
3
- /**
4
- * Process CPU percent the same way `top` reports it: CPU-ms / wall-ms * 100.
5
- * Can exceed 100 on multi-core. Pure so JVM tests stay cheap.
6
- */
7
- object ProcessCpu {
8
- fun percent(cpuDeltaMs: Long, wallDeltaMs: Long): Double =
9
- cpuDeltaMs * 100.0 / wallDeltaMs.coerceAtLeast(1L)
10
-
11
- fun label(percent: Double?, cores: Int): String {
12
- val proc = percent?.let { PipelineStats.format(it) } ?: "na"
13
- return "cpu{proc=$proc cores=$cores}"
14
- }
15
- }