@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,205 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.source
|
|
2
|
-
|
|
3
|
-
import com.mentra.acsmeeting.video.I420Packer
|
|
4
|
-
import java.nio.ByteBuffer
|
|
5
|
-
import kotlin.math.max
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Packed I420 generator. No Android / ACS / WebRTC types so JVM tests stay cheap.
|
|
9
|
-
*
|
|
10
|
-
* CHEAP paints a flat field, a moving bar, and a Recall-style stamp (6-digit
|
|
11
|
-
* frame id + 1s FPS). Low motion: enough change that ACS cannot skip, still
|
|
12
|
-
* trivially compressible.
|
|
13
|
-
*
|
|
14
|
-
* MOTION pans a tiled scene across the frame. That is the glasses-turn proxy:
|
|
15
|
-
* temporal prediction dies (almost every pixel changes) but spatial structure
|
|
16
|
-
* stays, so an I-frame still compresses. Use this to ask whether ACS holds
|
|
17
|
-
* 15 fps under high motion.
|
|
18
|
-
*
|
|
19
|
-
* NOISE rewrites the whole Y plane from an LCG. That is not camera motion.
|
|
20
|
-
* Neighbors are uncorrelated, so even I-frames stay huge. Measured on ACS at
|
|
21
|
-
* 720p it pinned ~1.9 Mbps and collapsed the wire rate to 1.4 fps while we
|
|
22
|
-
* submitted 15. Use it to stress the encoder, never to model glasses movement.
|
|
23
|
-
*
|
|
24
|
-
* Chroma stays at 128 in every mode.
|
|
25
|
-
*/
|
|
26
|
-
class SyntheticFrameFactory(
|
|
27
|
-
private val entropy: SyntheticEntropy = SyntheticEntropy.CHEAP,
|
|
28
|
-
) {
|
|
29
|
-
private var lastDest: ByteBuffer? = null
|
|
30
|
-
private var lastWidth = 0
|
|
31
|
-
private var lastHeight = 0
|
|
32
|
-
private var background: ByteArray? = null
|
|
33
|
-
private var scene: ByteArray? = null
|
|
34
|
-
private var sceneWidth = 0
|
|
35
|
-
private var sceneHeight = 0
|
|
36
|
-
|
|
37
|
-
fun write(dest: ByteBuffer, width: Int, height: Int, frameIndex: Int, emitFps: Double? = null) {
|
|
38
|
-
require(width > 0 && height > 0) { "size must be positive" }
|
|
39
|
-
val needed = I420Packer.packedSize(width, height)
|
|
40
|
-
require(dest.capacity() >= needed) { "dest capacity ${dest.capacity()} < $needed" }
|
|
41
|
-
when (entropy) {
|
|
42
|
-
SyntheticEntropy.NOISE -> writeNoise(dest, width, height, frameIndex)
|
|
43
|
-
SyntheticEntropy.MOTION -> writeMotion(dest, width, height, frameIndex)
|
|
44
|
-
SyntheticEntropy.CHEAP -> writeCheap(dest, width, height, frameIndex)
|
|
45
|
-
}
|
|
46
|
-
writeStamp(dest, width, height, frameIndex, emitFps)
|
|
47
|
-
dest.position(0)
|
|
48
|
-
dest.limit(needed)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun writeCheap(dest: ByteBuffer, width: Int, height: Int, frameIndex: Int) {
|
|
52
|
-
// Bulk-restore the field instead of a per-pixel loop: this runs on the emit
|
|
53
|
-
// thread every frame, and 921600 single-byte puts is exactly the Kotlin
|
|
54
|
-
// per-pixel cost the rest of this module exists to avoid.
|
|
55
|
-
val ySize = width * height
|
|
56
|
-
val template = background.takeIf { it != null && it.size == ySize }
|
|
57
|
-
?: ByteArray(ySize) { BG_Y }.also { background = it }
|
|
58
|
-
dest.clear()
|
|
59
|
-
dest.put(template, 0, ySize)
|
|
60
|
-
if (dest !== lastDest || width != lastWidth || height != lastHeight) {
|
|
61
|
-
fillChroma(dest, width, height)
|
|
62
|
-
lastDest = dest
|
|
63
|
-
lastWidth = width
|
|
64
|
-
lastHeight = height
|
|
65
|
-
}
|
|
66
|
-
val barWidth = max(8, width / 80)
|
|
67
|
-
val travel = max(1, width - barWidth)
|
|
68
|
-
val barX = ((frameIndex.toLong() * barWidth) % travel).toInt()
|
|
69
|
-
fillYRect(dest, width, height, barX, 0, barWidth, height, BAR_Y)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Copy a pre-painted tiled scene with a wrap-safe offset. One bulk put per
|
|
74
|
-
* row, not one put per pixel — the scene is the glasses-turn stand-in, and
|
|
75
|
-
* generating it in Kotlin every frame would be the cost we are trying to
|
|
76
|
-
* isolate out of this measurement.
|
|
77
|
-
*/
|
|
78
|
-
private fun writeMotion(dest: ByteBuffer, width: Int, height: Int, frameIndex: Int) {
|
|
79
|
-
val field = ensureScene(width, height)
|
|
80
|
-
val ox = ((frameIndex.toLong() * PAN_X) % TILE).toInt()
|
|
81
|
-
val oy = ((frameIndex.toLong() * PAN_Y) % TILE).toInt()
|
|
82
|
-
for (row in 0 until height) {
|
|
83
|
-
dest.position(row * width)
|
|
84
|
-
dest.put(field, (row + oy) * sceneWidth + ox, width)
|
|
85
|
-
}
|
|
86
|
-
if (dest !== lastDest || width != lastWidth || height != lastHeight) {
|
|
87
|
-
fillChroma(dest, width, height)
|
|
88
|
-
lastDest = dest
|
|
89
|
-
lastWidth = width
|
|
90
|
-
lastHeight = height
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private fun ensureScene(width: Int, height: Int): ByteArray {
|
|
95
|
-
val sw = width + TILE
|
|
96
|
-
val sh = height + TILE
|
|
97
|
-
val existing = scene
|
|
98
|
-
if (existing != null && sceneWidth == sw && sceneHeight == sh) return existing
|
|
99
|
-
val field = ByteArray(sw * sh)
|
|
100
|
-
for (y in 0 until sh) {
|
|
101
|
-
val rowOff = y * sw
|
|
102
|
-
val tileY = y / TILE
|
|
103
|
-
val gy = y % TILE
|
|
104
|
-
for (x in 0 until sw) {
|
|
105
|
-
val tile = (x / TILE + tileY) and 1
|
|
106
|
-
val gx = x % TILE
|
|
107
|
-
val base = if (tile == 0) DARK_TILE else LIGHT_TILE
|
|
108
|
-
field[rowOff + x] = (base + gx - TILE / 2 + gy / 4).toByte()
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
scene = field
|
|
112
|
-
sceneWidth = sw
|
|
113
|
-
sceneHeight = sh
|
|
114
|
-
return field
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private fun writeNoise(dest: ByteBuffer, width: Int, height: Int, frameIndex: Int) {
|
|
118
|
-
writeNoiseY(dest, width, height, frameIndex)
|
|
119
|
-
fillChroma(dest, width, height)
|
|
120
|
-
lastDest = dest
|
|
121
|
-
lastWidth = width
|
|
122
|
-
lastHeight = height
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private fun writeNoiseY(dest: ByteBuffer, width: Int, height: Int, frameIndex: Int) {
|
|
126
|
-
val ySize = width * height
|
|
127
|
-
var seed = (frameIndex + 1) * LCG_SEED
|
|
128
|
-
for (i in 0 until ySize) {
|
|
129
|
-
seed = seed * LCG_MUL + LCG_ADD
|
|
130
|
-
dest.put(i, ((seed ushr 24) and 0xFF).toByte())
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Recall burns `F 000123` plus live rates into the captured page. Same idea here:
|
|
136
|
-
* a 6-digit unique-frame id and the last 1s emit rate, readable on the Teams tile.
|
|
137
|
-
*/
|
|
138
|
-
private fun writeStamp(
|
|
139
|
-
dest: ByteBuffer,
|
|
140
|
-
width: Int,
|
|
141
|
-
height: Int,
|
|
142
|
-
frameIndex: Int,
|
|
143
|
-
emitFps: Double?,
|
|
144
|
-
) {
|
|
145
|
-
val scale = (height / 60).coerceIn(8, 16)
|
|
146
|
-
val line1 = "F ${frameIndex.coerceAtLeast(0).toString().padStart(6, '0')}"
|
|
147
|
-
val fpsLabel = emitFps?.let { ((it * 10).toInt() / 10.0).toString() } ?: "--"
|
|
148
|
-
val line2 = "FPS $fpsLabel"
|
|
149
|
-
val pad = scale * 2
|
|
150
|
-
val lineH = BitmapFont.textHeight(scale)
|
|
151
|
-
val gap = scale
|
|
152
|
-
val boxW = max(BitmapFont.textWidth(line1.length, scale), BitmapFont.textWidth(line2.length, scale)) + pad * 2
|
|
153
|
-
val boxH = lineH * 2 + gap + pad * 2
|
|
154
|
-
val boxX = (width / 16).coerceAtLeast(16)
|
|
155
|
-
val boxY = (height / 2 - boxH / 2).coerceAtLeast(0)
|
|
156
|
-
fillYRect(dest, width, height, boxX, boxY, boxW, boxH, OFF_Y)
|
|
157
|
-
BitmapFont.draw(dest, width, height, line1, boxX + pad, boxY + pad, scale, BAR_Y)
|
|
158
|
-
BitmapFont.draw(dest, width, height, line2, boxX + pad, boxY + pad + lineH + gap, scale, BAR_Y)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private fun fillChroma(dest: ByteBuffer, width: Int, height: Int) {
|
|
162
|
-
val ySize = width * height
|
|
163
|
-
val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
|
|
164
|
-
val end = ySize + 2 * uvSize
|
|
165
|
-
for (i in ySize until end) dest.put(i, CHROMA)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private fun fillYRect(
|
|
169
|
-
dest: ByteBuffer,
|
|
170
|
-
width: Int,
|
|
171
|
-
height: Int,
|
|
172
|
-
x0: Int,
|
|
173
|
-
y0: Int,
|
|
174
|
-
rectW: Int,
|
|
175
|
-
rectH: Int,
|
|
176
|
-
value: Byte,
|
|
177
|
-
) {
|
|
178
|
-
val xStart = x0.coerceIn(0, width)
|
|
179
|
-
val yStart = y0.coerceIn(0, height)
|
|
180
|
-
val xEnd = (x0 + rectW).coerceIn(0, width)
|
|
181
|
-
val yEnd = (y0 + rectH).coerceIn(0, height)
|
|
182
|
-
if (xStart >= xEnd || yStart >= yEnd) return
|
|
183
|
-
for (row in yStart until yEnd) {
|
|
184
|
-
val rowOff = row * width
|
|
185
|
-
for (col in xStart until xEnd) {
|
|
186
|
-
dest.put(rowOff + col, value)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
companion object {
|
|
192
|
-
const val BG_Y: Byte = 40
|
|
193
|
-
const val OFF_Y: Byte = 16
|
|
194
|
-
const val BAR_Y: Byte = 235.toByte()
|
|
195
|
-
const val CHROMA: Byte = 128.toByte()
|
|
196
|
-
const val TILE = 32
|
|
197
|
-
const val PAN_X = 8
|
|
198
|
-
const val PAN_Y = 4
|
|
199
|
-
private const val DARK_TILE = 70
|
|
200
|
-
private const val LIGHT_TILE = 170
|
|
201
|
-
private const val LCG_SEED = -1640531527
|
|
202
|
-
private const val LCG_MUL = 1664525
|
|
203
|
-
private const val LCG_ADD = 1013904223
|
|
204
|
-
}
|
|
205
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.source
|
|
2
|
-
|
|
3
|
-
import android.util.Log
|
|
4
|
-
import com.mentra.acsmeeting.telemetry.PipelineStats
|
|
5
|
-
import com.mentra.acsmeeting.video.I420Packer
|
|
6
|
-
import java.nio.ByteBuffer
|
|
7
|
-
import java.util.concurrent.Executors
|
|
8
|
-
import java.util.concurrent.ScheduledExecutorService
|
|
9
|
-
import java.util.concurrent.TimeUnit
|
|
10
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
-
import java.util.concurrent.atomic.AtomicInteger
|
|
12
|
-
import java.util.concurrent.atomic.AtomicLong
|
|
13
|
-
import java.util.concurrent.atomic.AtomicReference
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Fixed-rate packed-I420 source. Bypasses glasses, Cloudflare, WHEP and decode.
|
|
17
|
-
* Ticks that fail [isReady] or lack a target size are withheld and uncounted.
|
|
18
|
-
*/
|
|
19
|
-
class SyntheticI420Source(
|
|
20
|
-
private val video: VideoFrameListener,
|
|
21
|
-
private val stats: PipelineStats,
|
|
22
|
-
private val isReady: () -> Boolean,
|
|
23
|
-
private val config: SyntheticConfig = SyntheticConfig(),
|
|
24
|
-
) : GlassesMediaSource {
|
|
25
|
-
private val factory = SyntheticFrameFactory(config.entropy)
|
|
26
|
-
private val targetSize = AtomicReference<TargetSize?>(null)
|
|
27
|
-
private val pcmEnabled = AtomicBoolean(false)
|
|
28
|
-
private val frameIndex = AtomicInteger(0)
|
|
29
|
-
private val withheld = AtomicInteger(0)
|
|
30
|
-
private val withheldThisGap = AtomicInteger(0)
|
|
31
|
-
private val emitted = AtomicInteger(0)
|
|
32
|
-
private val emitting = AtomicBoolean(false)
|
|
33
|
-
private val firstLogged = AtomicBoolean(false)
|
|
34
|
-
private val fpsWindowStartNs = AtomicLong(0)
|
|
35
|
-
private val fpsWindowCount = AtomicInteger(0)
|
|
36
|
-
@Volatile private var stampFps: Double? = null
|
|
37
|
-
@Volatile override var state: SourceState = SourceState.IDLE
|
|
38
|
-
private set
|
|
39
|
-
@Volatile private var dest: ByteBuffer? = null
|
|
40
|
-
@Volatile private var destWidth = 0
|
|
41
|
-
@Volatile private var destHeight = 0
|
|
42
|
-
@Volatile private var destY: ByteBuffer? = null
|
|
43
|
-
@Volatile private var destU: ByteBuffer? = null
|
|
44
|
-
@Volatile private var destV: ByteBuffer? = null
|
|
45
|
-
private var scheduler: ScheduledExecutorService? = null
|
|
46
|
-
|
|
47
|
-
/** Test-visible gate transitions. Production also writes them to ACS-SPIKE. */
|
|
48
|
-
internal val events = mutableListOf<String>()
|
|
49
|
-
|
|
50
|
-
/** Puts the source in LIVE without starting the scheduler. For emitOnce tests. */
|
|
51
|
-
internal fun becomeLive() {
|
|
52
|
-
state = SourceState.LIVE
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
override fun start(config: SourceConfig) {
|
|
56
|
-
stop()
|
|
57
|
-
becomeLive()
|
|
58
|
-
val fps = this.config.fps.coerceAtLeast(1)
|
|
59
|
-
val periodUs = 1_000_000L / fps
|
|
60
|
-
val exec = Executors.newSingleThreadScheduledExecutor { runnable ->
|
|
61
|
-
Thread(runnable, "acs-synthetic-i420").apply { isDaemon = true }
|
|
62
|
-
}
|
|
63
|
-
scheduler = exec
|
|
64
|
-
exec.scheduleAtFixedRate({ emitOnce() }, 0, periodUs, TimeUnit.MICROSECONDS)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
override fun restart(config: SourceConfig) {
|
|
68
|
-
stop()
|
|
69
|
-
start(config)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
override fun stop() {
|
|
73
|
-
scheduler?.shutdownNow()
|
|
74
|
-
scheduler = null
|
|
75
|
-
frameIndex.set(0)
|
|
76
|
-
withheld.set(0)
|
|
77
|
-
withheldThisGap.set(0)
|
|
78
|
-
emitted.set(0)
|
|
79
|
-
emitting.set(false)
|
|
80
|
-
firstLogged.set(false)
|
|
81
|
-
fpsWindowStartNs.set(0)
|
|
82
|
-
fpsWindowCount.set(0)
|
|
83
|
-
stampFps = null
|
|
84
|
-
dest = null
|
|
85
|
-
destWidth = 0
|
|
86
|
-
destHeight = 0
|
|
87
|
-
destY = null
|
|
88
|
-
destU = null
|
|
89
|
-
destV = null
|
|
90
|
-
state = SourceState.IDLE
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
override fun setPcmDeliveryEnabled(enabled: Boolean) {
|
|
94
|
-
pcmEnabled.set(enabled)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
override fun setTargetSize(size: TargetSize?) {
|
|
98
|
-
targetSize.set(size)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
internal fun pcmDeliveryEnabled(): Boolean = pcmEnabled.get()
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Generate-and-emit one frame if the sender is ready and a target size exists.
|
|
105
|
-
* Returns true only when a frame was handed to [video].
|
|
106
|
-
*/
|
|
107
|
-
internal fun emitOnce(): Boolean {
|
|
108
|
-
if (state != SourceState.LIVE) return false
|
|
109
|
-
val size = targetSize.get()
|
|
110
|
-
val ready = isReady() && size != null
|
|
111
|
-
if (!ready) {
|
|
112
|
-
withheld.incrementAndGet()
|
|
113
|
-
withheldThisGap.incrementAndGet()
|
|
114
|
-
if (emitting.compareAndSet(true, false)) {
|
|
115
|
-
logEvent("P6 synthetic paused reason=not-ready afterFrames=${emitted.get()}")
|
|
116
|
-
}
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
val gap = withheldThisGap.getAndSet(0)
|
|
120
|
-
if (!emitting.get() && emitted.get() > 0) {
|
|
121
|
-
logEvent("P6 synthetic resumed withheld=$gap")
|
|
122
|
-
}
|
|
123
|
-
emitting.set(true)
|
|
124
|
-
|
|
125
|
-
val w = size!!.width
|
|
126
|
-
val h = size.height
|
|
127
|
-
val buffer = ensureDest(w, h)
|
|
128
|
-
val index = frameIndex.getAndIncrement()
|
|
129
|
-
stampFps = sampleStampFps()
|
|
130
|
-
factory.write(buffer, w, h, index, stampFps)
|
|
131
|
-
stats.onSink()
|
|
132
|
-
stats.recordGap()
|
|
133
|
-
stats.setSize(w, h)
|
|
134
|
-
val chroma = I420Packer.chromaStride(w)
|
|
135
|
-
video.onVideoFrame(
|
|
136
|
-
I420Planes(
|
|
137
|
-
y = destY!!,
|
|
138
|
-
strideY = w,
|
|
139
|
-
u = destU!!,
|
|
140
|
-
strideU = chroma,
|
|
141
|
-
v = destV!!,
|
|
142
|
-
strideV = chroma,
|
|
143
|
-
width = w,
|
|
144
|
-
height = h,
|
|
145
|
-
timestampNs = System.nanoTime(),
|
|
146
|
-
),
|
|
147
|
-
)
|
|
148
|
-
val count = emitted.incrementAndGet()
|
|
149
|
-
if (firstLogged.compareAndSet(false, true)) {
|
|
150
|
-
logEvent(
|
|
151
|
-
"P6 synthetic first-frame withheld=${withheld.get()} size=${w}x${h} " +
|
|
152
|
-
"fps=${config.fps} entropy=${config.entropy}",
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
return count > 0
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** 1-second emit rate, same window Recall uses for the P column on the stamp. */
|
|
159
|
-
private fun sampleStampFps(): Double? {
|
|
160
|
-
val now = System.nanoTime()
|
|
161
|
-
val started = fpsWindowStartNs.get()
|
|
162
|
-
if (started == 0L) {
|
|
163
|
-
fpsWindowStartNs.set(now)
|
|
164
|
-
fpsWindowCount.set(1)
|
|
165
|
-
return stampFps
|
|
166
|
-
}
|
|
167
|
-
fpsWindowCount.incrementAndGet()
|
|
168
|
-
val elapsedNs = now - started
|
|
169
|
-
if (elapsedNs >= 1_000_000_000L) {
|
|
170
|
-
val rate = fpsWindowCount.get() * 1_000_000_000.0 / elapsedNs
|
|
171
|
-
fpsWindowCount.set(0)
|
|
172
|
-
fpsWindowStartNs.set(now)
|
|
173
|
-
return rate
|
|
174
|
-
}
|
|
175
|
-
return stampFps
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private fun ensureDest(width: Int, height: Int): ByteBuffer {
|
|
179
|
-
val needed = I420Packer.packedSize(width, height)
|
|
180
|
-
val existing = dest
|
|
181
|
-
if (existing != null && destWidth == width && destHeight == height && existing.capacity() >= needed) {
|
|
182
|
-
existing.clear()
|
|
183
|
-
return existing
|
|
184
|
-
}
|
|
185
|
-
val next = ByteBuffer.allocateDirect(needed)
|
|
186
|
-
dest = next
|
|
187
|
-
destWidth = width
|
|
188
|
-
destHeight = height
|
|
189
|
-
slicePlanes(next, width, height)
|
|
190
|
-
return next
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private fun slicePlanes(packed: ByteBuffer, width: Int, height: Int) {
|
|
194
|
-
val ySize = width * height
|
|
195
|
-
val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
|
|
196
|
-
val view = packed.duplicate()
|
|
197
|
-
view.clear()
|
|
198
|
-
destY = view.duplicate().apply { position(0); limit(ySize) }.slice()
|
|
199
|
-
destU = view.duplicate().apply { position(ySize); limit(ySize + uvSize) }.slice()
|
|
200
|
-
destV = view.duplicate().apply { position(ySize + uvSize); limit(ySize + 2 * uvSize) }.slice()
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private fun logEvent(line: String) {
|
|
204
|
-
synchronized(events) { events.add(line) }
|
|
205
|
-
try {
|
|
206
|
-
Log.i(TAG, line)
|
|
207
|
-
} catch (_: Throwable) {
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
companion object {
|
|
212
|
-
private const val TAG = "ACS-SPIKE"
|
|
213
|
-
}
|
|
214
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.source
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dedupe WebRTC track attachments by native track id.
|
|
5
|
-
* Three observer callbacks can deliver distinct Java wrappers of one track;
|
|
6
|
-
* object identity cannot be used. [reset] is load-bearing: [CloudflareWhepSource.start]
|
|
7
|
-
* calls stop first, and a stale claim set makes reconnect never attach a sink. SoftAP
|
|
8
|
-
* WHIP renegotiate reuses the same track id (`video0`); without a reset the new peer
|
|
9
|
-
* decodes frames that never reach the ACS sink.
|
|
10
|
-
*/
|
|
11
|
-
class TrackRegistry {
|
|
12
|
-
private val ids = mutableSetOf<String>()
|
|
13
|
-
private var skipped = 0
|
|
14
|
-
|
|
15
|
-
@Synchronized
|
|
16
|
-
fun claim(id: String): Boolean {
|
|
17
|
-
if (ids.add(id)) return true
|
|
18
|
-
skipped += 1
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
@Synchronized
|
|
23
|
-
fun reset() {
|
|
24
|
-
ids.clear()
|
|
25
|
-
skipped = 0
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@Synchronized
|
|
29
|
-
fun size(): Int = ids.size
|
|
30
|
-
|
|
31
|
-
@Synchronized
|
|
32
|
-
fun skipped(): Int = skipped
|
|
33
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.source
|
|
2
|
-
|
|
3
|
-
enum class VideoSourceArm { WHEP, SYNTHETIC }
|
|
4
|
-
|
|
5
|
-
enum class SyntheticEntropy { CHEAP, MOTION, NOISE }
|
|
6
|
-
|
|
7
|
-
enum class DecoderMode { TEXTURE, BYTE_BUFFER }
|
|
8
|
-
|
|
9
|
-
enum class PixelFormatArm { I420, NV12 }
|
|
10
|
-
|
|
11
|
-
enum class OutgoingRateArm { ADVERTISE_REQUESTED, CLAMP_TO_SOFTWARE_CEILING }
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Investigation arm. SYNTHETIC bypasses glasses, Cloudflare, WHEP and decode.
|
|
15
|
-
* Ships as WHEP. Flip locally to run the experiment; do not commit SYNTHETIC.
|
|
16
|
-
*
|
|
17
|
-
* [decoderMode] TEXTURE is the production default (shared EGL, MediaCodec to
|
|
18
|
-
* Surface). BYTE_BUFFER is the A/B: no shared context, CPU planes, no
|
|
19
|
-
* glReadPixels. 720p BYTE_BUFFER on SM_S948U decoded on hardware with
|
|
20
|
-
* i420P95=0, but wire/codec never attached and busy drops climbed. Do not
|
|
21
|
-
* commit BYTE_BUFFER until a same-phone 540p A/B clears the campaign gates.
|
|
22
|
-
*
|
|
23
|
-
* [zeroCopy] hands WebRTC I420 planes straight to ACS when they are tight and
|
|
24
|
-
* retainable. Ships off. Do not commit true.
|
|
25
|
-
*
|
|
26
|
-
* [pixelFormat] I420 is the production default. NV12 is the encoder-flip A/B:
|
|
27
|
-
* advertise and send biplanar NV12 in case ACS picks a hardware H.264
|
|
28
|
-
* encoder. Revert unless `codecName` leaves `h264 sw`. Do not commit NV12
|
|
29
|
-
* on a failed flip.
|
|
30
|
-
*/
|
|
31
|
-
object AcsInvestigation {
|
|
32
|
-
val videoArm = VideoSourceArm.WHEP
|
|
33
|
-
val syntheticFps = 15
|
|
34
|
-
val syntheticEntropy = SyntheticEntropy.MOTION
|
|
35
|
-
val decoderMode = DecoderMode.TEXTURE
|
|
36
|
-
val zeroCopy = false
|
|
37
|
-
val pixelFormat = PixelFormatArm.I420
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The rate we declare to ACS.
|
|
41
|
-
*
|
|
42
|
-
* CLAMP_TO_SOFTWARE_CEILING pins it to [VideoProfile.SOFTWARE_ENCODER_FPS]
|
|
43
|
-
* whenever the profile asks for more. It was the default on the strength of
|
|
44
|
-
* soaks that reported `codecName: "h264 sw"` alongside wire ≈ 8.8 fps, read as
|
|
45
|
-
* an encoder ceiling. Two things in the device logs say it was never the
|
|
46
|
-
* encoder.
|
|
47
|
-
*
|
|
48
|
-
* 720p15 and 540p15 both reported ~8.8 fps. 540p is 52% of the pixels of 720p,
|
|
49
|
-
* so a CPU-bound encoder would have been close to twice as fast at the smaller
|
|
50
|
-
* size; a limit that does not move with pixel count is not a pixel cost. And
|
|
51
|
-
* once [FramePacer] delivered a steady cadence, `wire` tracked what we handed
|
|
52
|
-
* ACS — 6.9 against 7.1 admitted — which is an encoder keeping up, not one
|
|
53
|
-
* saturating. Both of those soaks ran through a send path that could not hold a
|
|
54
|
-
* rate, so what they measured was our delivery.
|
|
55
|
-
*
|
|
56
|
-
* ADVERTISE_REQUESTED runs the configuration that was never actually tried:
|
|
57
|
-
* declare the profile's own fps and let the corrected pacer deliver it. Read
|
|
58
|
-
* the `P7 rate` verdict for what binds; it distinguishes an encoder that cannot
|
|
59
|
-
* keep up from a rate controller starving one that can, and those two want
|
|
60
|
-
* opposite fixes.
|
|
61
|
-
*/
|
|
62
|
-
val outgoingRate = OutgoingRateArm.ADVERTISE_REQUESTED
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* libwebrtc's own `LS_INFO` stream during a SoftAP call.
|
|
66
|
-
*
|
|
67
|
-
* The one place that says which interfaces `BasicNetworkManager` kept, what adapter type it gave
|
|
68
|
-
* each one, and every `BindSocketToNetwork` result — the evidence that separated a hotspot the
|
|
69
|
-
* kernel had from one libwebrtc had erased. It is also loud, and it is the native stack's whole
|
|
70
|
-
* log, not ours.
|
|
71
|
-
*
|
|
72
|
-
* Flip to false once the SoftAP path is green on device. Keep the `NetworkFacts` stages either
|
|
73
|
-
* way: they are ours, bounded, and the thing that makes the next regression readable.
|
|
74
|
-
*/
|
|
75
|
-
const val LIBWEBRTC_VERBOSE = true
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* How long to hold outgoing microphone PCM before the resampler, to align the wearer's voice
|
|
79
|
-
* with their video at the far end.
|
|
80
|
-
*
|
|
81
|
-
* Ships at 0, and 0 is not a placeholder: the audio and the video reach ACS over different
|
|
82
|
-
* transports (BLE LC3 versus SoftAP WebRTC) with different latencies, but *how* different is a
|
|
83
|
-
* receiver-side measurement, not something either side can infer. Deriving it from first-frame
|
|
84
|
-
* arrival times measures startup skew and would be wrong by however long the camera took to boot.
|
|
85
|
-
*
|
|
86
|
-
* Calibrate the ingest path with a clap in frame and read `AVSYNC clap audioLeadMs`. That is
|
|
87
|
-
* the offset the delay line can cancel. ACS/Teams jitter after ingest still needs a Teams
|
|
88
|
-
* recording. It is logged as `configuredDelayMs` and deliberately never reported as a measured
|
|
89
|
-
* offset.
|
|
90
|
-
*/
|
|
91
|
-
const val acsAudioDelayMs = 0
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Whether outgoing audio buffers carry a presentation timestamp.
|
|
95
|
-
*
|
|
96
|
-
* Whether ACS honours these for lip-sync on *raw* audio is unanswered — the SDK exposes the
|
|
97
|
-
* setter and documents nothing about the receiver. The only way to find out is to record the
|
|
98
|
-
* Teams receiver with this on and with it off and compare, so it has to be switchable; a constant
|
|
99
|
-
* true would leave two of the four measurement configurations unreachable.
|
|
100
|
-
*
|
|
101
|
-
* Defaults to true because the fallback is worse for an unrelated reason: a run of zero
|
|
102
|
-
* timestamps is what makes the Teams jitter buffer hold.
|
|
103
|
-
*/
|
|
104
|
-
const val acsAudioTimestamps = true
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
data class SyntheticConfig(
|
|
108
|
-
val fps: Int = AcsInvestigation.syntheticFps,
|
|
109
|
-
val entropy: SyntheticEntropy = AcsInvestigation.syntheticEntropy,
|
|
110
|
-
)
|