@mentra/acs-meeting 3.2.0-dev.120
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/NOTICE +20 -0
- package/README.md +412 -0
- package/THIRD_PARTY +7 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +80 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +846 -0
- package/android/src/main/java/com/mentra/acsmeeting/RemoteRoster.kt +123 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +90 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AudioPolicyApplier.kt +176 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingAudioPump.kt +112 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingRateProbe.kt +65 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +108 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmResampler.kt +159 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PhoneMicCapturer.kt +115 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +302 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +230 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +99 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +578 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +45 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +101 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +57 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +205 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +214 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +31 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +41 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +78 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +281 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +15 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +33 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +475 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsTimestamp.kt +26 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +22 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +90 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +72 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12FormatSpec.kt +36 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/SendGate.kt +27 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +57 -0
- package/android/src/test/java/com/mentra/acsmeeting/RemoteRosterTest.kt +40 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AcsAudioPolicyTest.kt +204 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AudioPolicyApplierTest.kt +223 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +12 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingAudioPumpTest.kt +97 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingRateProbeTest.kt +78 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/PcmResamplerTest.kt +125 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +201 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +166 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +26 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +22 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +60 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +133 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +67 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +193 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +176 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +36 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +61 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +256 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +18 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +49 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +43 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +33 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +77 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +126 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/Nv12FormatSpecTest.kt +36 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/Nv12PackerTest.kt +58 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/SendGateTest.kt +79 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +68 -0
- package/build/AcsMeeting.types.d.ts +59 -0
- package/build/AcsMeeting.types.d.ts.map +1 -0
- package/build/AcsMeeting.types.js +2 -0
- package/build/AcsMeeting.types.js.map +1 -0
- package/build/AcsMeetingModule.d.ts +15 -0
- package/build/AcsMeetingModule.d.ts.map +1 -0
- package/build/AcsMeetingModule.js +3 -0
- package/build/AcsMeetingModule.js.map +1 -0
- package/build/AcsMeetingModule.web.d.ts +16 -0
- package/build/AcsMeetingModule.web.d.ts.map +1 -0
- package/build/AcsMeetingModule.web.js +31 -0
- package/build/AcsMeetingModule.web.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AcsFrameSender.swift +47 -0
- package/ios/AcsMeeting.podspec +27 -0
- package/ios/AcsMeetingModule.swift +558 -0
- package/ios/PcmBridge.swift +117 -0
- package/ios/PhoneMicCapturer.swift +64 -0
- package/ios/PolicyKit/Package.swift +17 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/AcsAudioPolicy.swift +102 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/AudioPolicyApplier.swift +162 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +68 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +273 -0
- package/ios/WhepVideoSource.swift +356 -0
- package/package.json +56 -0
- package/src/AcsMeeting.types.ts +66 -0
- package/src/AcsMeetingModule.ts +16 -0
- package/src/AcsMeetingModule.web.ts +33 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
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 abandoned = AtomicInteger(0)
|
|
28
|
+
private val dup = AtomicInteger(0)
|
|
29
|
+
private val rot = AtomicInteger(0)
|
|
30
|
+
|
|
31
|
+
private val destAlloc = AtomicInteger(0)
|
|
32
|
+
private val planeAlloc = AtomicInteger(0)
|
|
33
|
+
|
|
34
|
+
private val lastTickSink = AtomicInteger(0)
|
|
35
|
+
private val lastTickSub = AtomicInteger(0)
|
|
36
|
+
private val lastTickMs = AtomicLong(0)
|
|
37
|
+
|
|
38
|
+
private val lastArrivalNs = AtomicLong(0)
|
|
39
|
+
|
|
40
|
+
val gap = RingPercentile()
|
|
41
|
+
val pack = RingPercentile()
|
|
42
|
+
val scale = RingPercentile()
|
|
43
|
+
val toI420 = RingPercentile()
|
|
44
|
+
val sinkCb = RingPercentile()
|
|
45
|
+
val split = RingPercentile()
|
|
46
|
+
val copy = RingPercentile()
|
|
47
|
+
val send = RingPercentile()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* WebRTC inbound-rtp disposition. Cumulative; the ladder prints deltas so a
|
|
51
|
+
* climbing counter is visible without reading absolute values.
|
|
52
|
+
*/
|
|
53
|
+
data class RecvHealth(
|
|
54
|
+
val assembled: Long = -1L,
|
|
55
|
+
val dropped: Long = -1L,
|
|
56
|
+
val packetsLost: Long = -1L,
|
|
57
|
+
val nack: Long = -1L,
|
|
58
|
+
val pli: Long = -1L,
|
|
59
|
+
val freezes: Long = -1L,
|
|
60
|
+
val freezeSec: Double = -1.0,
|
|
61
|
+
val jitter: Double = -1.0,
|
|
62
|
+
val decodeSec: Double = -1.0,
|
|
63
|
+
val jitterBufferSec: Double = -1.0,
|
|
64
|
+
val jitterBufferEmits: Long = -1L,
|
|
65
|
+
val decImpl: String = "",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@Volatile var arm: String = "whep"
|
|
69
|
+
/** texture | bytebuf — which decoder factory we built. */
|
|
70
|
+
@Volatile var pathMode: String = "texture"
|
|
71
|
+
/** packsplit | planes | zerocopy | nv12 — how pixels reach ACS. */
|
|
72
|
+
@Volatile var pathCopy: String = "packsplit"
|
|
73
|
+
/** i420 | nv12 — pixel format advertised to ACS. */
|
|
74
|
+
@Volatile var pix: String = "i420"
|
|
75
|
+
/** MEDIA_STATISTICS codecName, underscored. Empty until the first report. */
|
|
76
|
+
@Volatile var codecName: String = ""
|
|
77
|
+
@Volatile var decodedFps: Double? = null
|
|
78
|
+
@Volatile var recvFps: Double? = null
|
|
79
|
+
@Volatile var wireFps: Double? = null
|
|
80
|
+
@Volatile var wireWidth: Int? = null
|
|
81
|
+
@Volatile var wireHeight: Int? = null
|
|
82
|
+
@Volatile var wireBitrateBps: Long? = null
|
|
83
|
+
@Volatile private var recv: RecvHealth? = null
|
|
84
|
+
@Volatile private var recvPrev: RecvHealth? = null
|
|
85
|
+
@Volatile var width: Int = 0
|
|
86
|
+
@Volatile var height: Int = 0
|
|
87
|
+
@Volatile var chromaY: Int = 0
|
|
88
|
+
@Volatile var chromaU: Int = 0
|
|
89
|
+
@Volatile var chromaV: Int = 0
|
|
90
|
+
@Volatile var strideY: Int = 0
|
|
91
|
+
@Volatile var strideU: Int = 0
|
|
92
|
+
@Volatile var strideV: Int = 0
|
|
93
|
+
@Volatile var zcOn: Int = 0
|
|
94
|
+
|
|
95
|
+
private val bufTex = AtomicInteger(0)
|
|
96
|
+
private val bufI420 = AtomicInteger(0)
|
|
97
|
+
private val bufOther = AtomicInteger(0)
|
|
98
|
+
private val strideTight = AtomicInteger(0)
|
|
99
|
+
private val stridePadded = AtomicInteger(0)
|
|
100
|
+
private val zcUsed = AtomicInteger(0)
|
|
101
|
+
private val zcFell = AtomicInteger(0)
|
|
102
|
+
private val zcPadded = AtomicInteger(0)
|
|
103
|
+
private val zcHeldMax = AtomicInteger(0)
|
|
104
|
+
private val zcTimeout = AtomicInteger(0)
|
|
105
|
+
|
|
106
|
+
fun onSink() {
|
|
107
|
+
sink.incrementAndGet()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun recordGap(nowNs: Long = System.nanoTime()) {
|
|
111
|
+
val prev = lastArrivalNs.getAndSet(nowNs)
|
|
112
|
+
if (prev != 0L) gap.record(nowNs - prev)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Kept as the explicit hand-off point; inFlight is derived, so this records nothing. */
|
|
116
|
+
fun onQueued() = Unit
|
|
117
|
+
|
|
118
|
+
fun onSub() {
|
|
119
|
+
sub.incrementAndGet()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fun onDropSize() = dropSize.incrementAndGet()
|
|
123
|
+
fun onDropBusy() = dropBusy.incrementAndGet()
|
|
124
|
+
fun onDropNotStarted() = dropNotStarted.incrementAndGet()
|
|
125
|
+
fun onDropFail() = dropFail.incrementAndGet()
|
|
126
|
+
fun onDropMalformed() = dropFail.incrementAndGet()
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A send that timed out. The future is not cancelled, so ACS may still read those
|
|
130
|
+
* direct buffers; they are never recycled. Non-zero here means the pool is leaking.
|
|
131
|
+
*/
|
|
132
|
+
fun onAbandoned() = abandoned.incrementAndGet()
|
|
133
|
+
|
|
134
|
+
fun abandonedCount(): Int = abandoned.get()
|
|
135
|
+
fun onDropNullI420() = dropNullI420.incrementAndGet()
|
|
136
|
+
fun onDup() = dup.incrementAndGet()
|
|
137
|
+
fun onRotation() = rot.incrementAndGet()
|
|
138
|
+
fun onDestAlloc() = destAlloc.incrementAndGet()
|
|
139
|
+
fun onPlaneAlloc() = planeAlloc.incrementAndGet()
|
|
140
|
+
fun destAllocCount(): Int = destAlloc.get()
|
|
141
|
+
fun planeAllocCount(): Int = planeAlloc.get()
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Classify the WebRTC [VideoFrame.Buffer] without importing WebRTC here.
|
|
145
|
+
* [kind] is "tex", "i420", or anything else ("other").
|
|
146
|
+
*/
|
|
147
|
+
fun onFrameBuffer(kind: String) {
|
|
148
|
+
when (kind) {
|
|
149
|
+
"tex" -> bufTex.incrementAndGet()
|
|
150
|
+
"i420" -> bufI420.incrementAndGet()
|
|
151
|
+
else -> bufOther.incrementAndGet()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fun onStrides(strideY: Int, strideU: Int, strideV: Int, width: Int) {
|
|
156
|
+
this.strideY = strideY
|
|
157
|
+
this.strideU = strideU
|
|
158
|
+
this.strideV = strideV
|
|
159
|
+
val chroma = I420Packer.chromaStride(width)
|
|
160
|
+
if (strideY == width && strideU == chroma && strideV == chroma) {
|
|
161
|
+
strideTight.incrementAndGet()
|
|
162
|
+
} else {
|
|
163
|
+
stridePadded.incrementAndGet()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun onZcUsed(heldNow: Int) {
|
|
168
|
+
zcUsed.incrementAndGet()
|
|
169
|
+
zcHeldMax.updateAndGet { current -> if (heldNow > current) heldNow else current }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun onZcFell() = zcFell.incrementAndGet()
|
|
173
|
+
fun onZcPadded() = zcPadded.incrementAndGet()
|
|
174
|
+
fun onZcTimeout() = zcTimeout.incrementAndGet()
|
|
175
|
+
|
|
176
|
+
fun zcUsedCount(): Int = zcUsed.get()
|
|
177
|
+
fun zcFellCount(): Int = zcFell.get()
|
|
178
|
+
fun zcTimeoutCount(): Int = zcTimeout.get()
|
|
179
|
+
fun zcHeldMax(): Int = zcHeldMax.get()
|
|
180
|
+
fun strideTightCount(): Int = strideTight.get()
|
|
181
|
+
fun stridePaddedCount(): Int = stridePadded.get()
|
|
182
|
+
fun bufTexCount(): Int = bufTex.get()
|
|
183
|
+
fun bufI420Count(): Int = bufI420.get()
|
|
184
|
+
fun bufOtherCount(): Int = bufOther.get()
|
|
185
|
+
|
|
186
|
+
fun setSize(w: Int, h: Int) {
|
|
187
|
+
width = w
|
|
188
|
+
height = h
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fun setRecvHealth(next: RecvHealth) {
|
|
192
|
+
recvPrev = recv
|
|
193
|
+
recv = next
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Per-tick deltas plus the two absolutes that only mean something cumulatively. */
|
|
197
|
+
fun recvLabel(): String {
|
|
198
|
+
val now = recv ?: return "recv{na}"
|
|
199
|
+
val was = recvPrev
|
|
200
|
+
fun d(pick: (RecvHealth) -> Long): String {
|
|
201
|
+
val current = pick(now)
|
|
202
|
+
if (current < 0) return "na"
|
|
203
|
+
val previous = was?.let(pick) ?: return current.toString()
|
|
204
|
+
return if (previous < 0) current.toString() else (current - previous).toString()
|
|
205
|
+
}
|
|
206
|
+
val decodeMs = if (now.decodeSec < 0 || now.assembled <= 0) "na"
|
|
207
|
+
else format(now.decodeSec * 1000.0 / now.assembled)
|
|
208
|
+
val jbMs = if (now.jitterBufferSec < 0 || now.jitterBufferEmits <= 0) "na"
|
|
209
|
+
else format(now.jitterBufferSec * 1000.0 / now.jitterBufferEmits)
|
|
210
|
+
val impl = now.decImpl.ifBlank { "na" }.replace(' ', '_')
|
|
211
|
+
return "recv{drop=${d { it.dropped }} lost=${d { it.packetsLost }} nack=${d { it.nack }} " +
|
|
212
|
+
"pli=${d { it.pli }} freeze=${d { it.freezes }} freezeSec=${format(now.freezeSec)} " +
|
|
213
|
+
"jit=${format(now.jitter * 1000.0)} decMs=$decodeMs jbMs=$jbMs decImpl=$impl}"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fun setChroma(sample: ChromaProbe.Sample) {
|
|
217
|
+
chromaY = sample.y
|
|
218
|
+
chromaU = sample.u
|
|
219
|
+
chromaV = sample.v
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fun sinkCount(): Int = sink.get()
|
|
223
|
+
fun subCount(): Int = sub.get()
|
|
224
|
+
fun dropCount(): Int = dropSize.get() + dropBusy.get() + dropNotStarted.get() + dropFail.get() + dropNullI420.get()
|
|
225
|
+
|
|
226
|
+
/** Read sink last: every frame hits sink before sub or drop, so this cannot go negative. */
|
|
227
|
+
fun inFlightCount(): Int {
|
|
228
|
+
val settled = sub.get() + dropCount()
|
|
229
|
+
return sink.get() - settled
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun dupCount(): Int = dup.get()
|
|
233
|
+
fun rotCount(): Int = rot.get()
|
|
234
|
+
|
|
235
|
+
fun conserved(): Boolean = inFlightCount() >= 0
|
|
236
|
+
|
|
237
|
+
fun tick(): String {
|
|
238
|
+
val now = nowMs()
|
|
239
|
+
val previous = lastTickMs.get()
|
|
240
|
+
val dt = if (previous == 0L) 1000L else (now - previous).coerceAtLeast(1L)
|
|
241
|
+
lastTickMs.set(now)
|
|
242
|
+
val sinkNow = sink.get()
|
|
243
|
+
val subNow = sub.get()
|
|
244
|
+
val sinkDelta = sinkNow - lastTickSink.getAndSet(sinkNow)
|
|
245
|
+
val subDelta = subNow - lastTickSub.getAndSet(subNow)
|
|
246
|
+
val sinkRate = rate(sinkDelta, dt)
|
|
247
|
+
val subRate = rate(subDelta, dt)
|
|
248
|
+
val dec = decodedFps?.let { formatRate(it) } ?: "na"
|
|
249
|
+
val rcv = recvFps?.let { formatRate(it) } ?: "na"
|
|
250
|
+
val wireFpsLabel = wireFps?.let { formatRate(it) } ?: "na"
|
|
251
|
+
val wireW = wireWidth ?: 0
|
|
252
|
+
val wireH = wireHeight ?: 0
|
|
253
|
+
val wireKbps = wireBitrateBps?.let { (it / 1000).toString() } ?: "na"
|
|
254
|
+
val codec = codecName.ifBlank { "na" }.replace(' ', '_')
|
|
255
|
+
val wire = "${wireW}x${wireH}@$wireFpsLabel kbps=$wireKbps codec=$codec"
|
|
256
|
+
val sizeLabel = if (width > 0 && height > 0) "${width}x${height}" else "0x0"
|
|
257
|
+
val inFlightNow = inFlightCount()
|
|
258
|
+
val conserve = if (inFlightNow >= 0) "" else " CONSERVE_FAIL"
|
|
259
|
+
return "P6 ladder arm=$arm $sizeLabel recv=$rcv dec=$dec sink=$sinkRate dup=${dup.get()} sub=$subRate wire=$wire rot=${rot.get()} " +
|
|
260
|
+
"drop{size=${dropSize.get()} busy=${dropBusy.get()} notStarted=${dropNotStarted.get()} fail=${dropFail.get()} nullI420=${dropNullI420.get()} abandoned=${abandoned.get()}} " +
|
|
261
|
+
"${recvLabel()} " +
|
|
262
|
+
"path{mode=$pathMode copy=$pathCopy pix=$pix} " +
|
|
263
|
+
"buf{tex=${bufTex.get()} i420=${bufI420.get()} other=${bufOther.get()}} " +
|
|
264
|
+
"stride{y=$strideY u=$strideU v=$strideV tight=${strideTight.get()} padded=${stridePadded.get()}} " +
|
|
265
|
+
"zc{on=$zcOn used=${zcUsed.get()} fell=${zcFell.get()} padded=${zcPadded.get()} heldMax=${zcHeldMax.get()} timeout=${zcTimeout.get()}} " +
|
|
266
|
+
"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()} sendP95=${send.p95()}} " +
|
|
267
|
+
"alloc{dest=${destAlloc.get()} plane=${planeAlloc.get()}} " +
|
|
268
|
+
"chroma{y=$chromaY u=$chromaU v=$chromaV} " +
|
|
269
|
+
"cum{sink=$sinkNow sub=$subNow drop=${dropCount()} inFlight=$inFlightNow}" +
|
|
270
|
+
conserve
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private fun rate(delta: Int, dtMs: Long): String = formatRate(delta * 1000.0 / dtMs)
|
|
274
|
+
|
|
275
|
+
companion object {
|
|
276
|
+
fun formatRate(value: Double): String = ((value * 10).roundToInt() / 10.0).toString()
|
|
277
|
+
|
|
278
|
+
fun format(value: Double): String =
|
|
279
|
+
if (value < 0) "na" else ((value * 10).roundToInt() / 10.0).toString()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
looper: Looper = Looper.getMainLooper(),
|
|
11
|
+
private val elapsedCpuMs: () -> Long = { Process.getElapsedCpuTime() },
|
|
12
|
+
private val cores: Int = Runtime.getRuntime().availableProcessors(),
|
|
13
|
+
private val emit: (String) -> Unit,
|
|
14
|
+
) {
|
|
15
|
+
private val handler = Handler(looper)
|
|
16
|
+
private var lastCpuMs = -1L
|
|
17
|
+
private var lastWallMs = 0L
|
|
18
|
+
private val tick = object : Runnable {
|
|
19
|
+
override fun run() {
|
|
20
|
+
emit("${stats.tick()} ${cpuLabel()}")
|
|
21
|
+
handler.postDelayed(this, INTERVAL_MS)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun start() {
|
|
26
|
+
lastCpuMs = -1L
|
|
27
|
+
lastWallMs = 0L
|
|
28
|
+
handler.removeCallbacks(tick)
|
|
29
|
+
handler.post(tick)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun stop() {
|
|
33
|
+
handler.removeCallbacks(tick)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun cpuLabel(): String {
|
|
37
|
+
val now = System.currentTimeMillis()
|
|
38
|
+
val cpu = elapsedCpuMs()
|
|
39
|
+
val prevCpu = lastCpuMs
|
|
40
|
+
val prevWall = lastWallMs
|
|
41
|
+
lastCpuMs = cpu
|
|
42
|
+
lastWallMs = now
|
|
43
|
+
val percent = if (prevCpu < 0 || prevWall <= 0) null
|
|
44
|
+
else ProcessCpu.percent(cpu - prevCpu, now - prevWall)
|
|
45
|
+
return ProcessCpu.label(percent, cores)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
companion object {
|
|
49
|
+
const val INTERVAL_MS = 1000L
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.telemetry
|
|
2
|
+
|
|
3
|
+
import kotlin.math.roundToInt
|
|
4
|
+
|
|
5
|
+
/** Thread-safe ring of nanosecond samples. p50/p95 for the 1 Hz ladder. */
|
|
6
|
+
class RingPercentile(private val capacity: Int = 32) {
|
|
7
|
+
private val samples = LongArray(capacity)
|
|
8
|
+
private var count = 0
|
|
9
|
+
private var index = 0
|
|
10
|
+
|
|
11
|
+
@Synchronized
|
|
12
|
+
fun record(ns: Long) {
|
|
13
|
+
samples[index] = ns
|
|
14
|
+
index = (index + 1) % capacity
|
|
15
|
+
if (count < capacity) count += 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fun p50(): String = percentile(0.50)
|
|
19
|
+
|
|
20
|
+
fun p95(): String = percentile(0.95)
|
|
21
|
+
|
|
22
|
+
@Synchronized
|
|
23
|
+
fun percentile(quantile: Double): String {
|
|
24
|
+
if (count == 0) return "na"
|
|
25
|
+
val copy = samples.copyOf(count)
|
|
26
|
+
copy.sort()
|
|
27
|
+
val idx = ((copy.size - 1) * quantile).roundToInt().coerceIn(0, copy.lastIndex)
|
|
28
|
+
return ((copy[idx] / 1_000_000.0) * 10).roundToInt().div(10.0).toString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Synchronized
|
|
32
|
+
fun size(): Int = count
|
|
33
|
+
}
|