@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.
- package/README.md +14 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +10 -10
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +37 -37
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +4 -4
- package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +3 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +12 -10
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +2 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/{AcsInvestigationTest.kt → MediaDiagnosticsTest.kt} +10 -10
- package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +3 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +2 -2
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +2 -2
- package/build/AcsMeeting.types.d.ts +10 -1
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +2 -2
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/ios/AcsMeeting.podspec +1 -1
- package/ios/AcsMeetingModule.swift +284 -148
- package/ios/PolicyKit/Package.swift +2 -1
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CallbackOperation.swift +60 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CapturePolicy.swift +5 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +1 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/CallAgentRetirementTests.swift +113 -0
- package/package.json +3 -2
- package/src/AcsMeeting.types.ts +4 -1
- package/src/AcsMeetingModule.ts +2 -2
- package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +0 -291
- package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +0 -501
- package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +0 -51
- package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +0 -81
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +0 -355
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +0 -109
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +0 -103
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +0 -53
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +0 -127
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +0 -573
- package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +0 -99
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +0 -487
- package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +0 -147
- package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +0 -45
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +0 -772
- package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +0 -57
- package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +0 -17
- package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +0 -139
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +0 -144
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +0 -46
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +0 -205
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +0 -214
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +0 -33
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +0 -110
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +0 -207
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +0 -356
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +0 -155
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +0 -78
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +0 -372
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +0 -79
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +0 -15
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +0 -48
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +0 -178
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +0 -145
- package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +0 -22
- package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +0 -72
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +0 -102
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +0 -35
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +0 -96
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +0 -187
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +0 -172
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +0 -141
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +0 -69
- package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +0 -22
- package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +0 -41
- package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +0 -60
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +0 -151
- package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +0 -67
- package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +0 -76
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +0 -275
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +0 -193
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +0 -176
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +0 -45
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +0 -356
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +0 -411
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +0 -83
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +0 -61
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +0 -322
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +0 -18
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +0 -49
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +0 -149
- package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +0 -97
- package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +0 -33
- package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +0 -126
- package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +0 -68
- 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
|
-
}
|