@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,61 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.telemetry
|
|
2
|
+
|
|
3
|
+
import com.mentra.acsmeeting.video.I420Packer
|
|
4
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import java.nio.ByteBuffer
|
|
7
|
+
|
|
8
|
+
class ChromaProbeTest {
|
|
9
|
+
@Test
|
|
10
|
+
fun packedNeutralAveragesNearMidChroma() {
|
|
11
|
+
val width = 8
|
|
12
|
+
val height = 8
|
|
13
|
+
val packed = ByteBuffer.allocate(I420Packer.packedSize(width, height))
|
|
14
|
+
val ySize = width * height
|
|
15
|
+
val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
|
|
16
|
+
repeat(ySize) { packed.put(100.toByte()) }
|
|
17
|
+
repeat(uvSize) { packed.put(128.toByte()) }
|
|
18
|
+
repeat(uvSize) { packed.put(128.toByte()) }
|
|
19
|
+
packed.flip()
|
|
20
|
+
val sample = ChromaProbe.samplePacked(packed, width, height)
|
|
21
|
+
assertThat(sample.y).isEqualTo(100)
|
|
22
|
+
assertThat(sample.u).isEqualTo(128)
|
|
23
|
+
assertThat(sample.v).isEqualTo(128)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun samplePlanesAgreesWithSamplePacked() {
|
|
28
|
+
val width = 8
|
|
29
|
+
val height = 8
|
|
30
|
+
val packed = ByteBuffer.allocate(I420Packer.packedSize(width, height))
|
|
31
|
+
val ySize = width * height
|
|
32
|
+
val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
|
|
33
|
+
repeat(ySize) { packed.put(90.toByte()) }
|
|
34
|
+
repeat(uvSize) { packed.put(130.toByte()) }
|
|
35
|
+
repeat(uvSize) { packed.put(126.toByte()) }
|
|
36
|
+
packed.flip()
|
|
37
|
+
val packedSample = ChromaProbe.samplePacked(packed, width, height)
|
|
38
|
+
val y = packed.duplicate().apply { position(0); limit(ySize) }.slice()
|
|
39
|
+
val u = packed.duplicate().apply { position(ySize); limit(ySize + uvSize) }.slice()
|
|
40
|
+
val v = packed.duplicate().apply { position(ySize + uvSize); limit(ySize + 2 * uvSize) }.slice()
|
|
41
|
+
val planeSample = ChromaProbe.samplePlanes(y, u, v)
|
|
42
|
+
assertThat(planeSample).isEqualTo(packedSample)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun sampleNv12SeparatesInterleavedChroma() {
|
|
47
|
+
val y = ByteBuffer.allocate(8)
|
|
48
|
+
repeat(8) { y.put(100.toByte()) }
|
|
49
|
+
y.flip()
|
|
50
|
+
val uv = ByteBuffer.allocate(8)
|
|
51
|
+
for (i in 0 until 4) {
|
|
52
|
+
uv.put(130.toByte())
|
|
53
|
+
uv.put(126.toByte())
|
|
54
|
+
}
|
|
55
|
+
uv.flip()
|
|
56
|
+
val sample = ChromaProbe.sampleNv12(y, uv)
|
|
57
|
+
assertThat(sample.y).isEqualTo(100)
|
|
58
|
+
assertThat(sample.u).isEqualTo(130)
|
|
59
|
+
assertThat(sample.v).isEqualTo(126)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.telemetry
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class PipelineStatsTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun conservationHoldsAcrossEveryDisposition() {
|
|
9
|
+
val stats = PipelineStats()
|
|
10
|
+
repeat(4) { stats.onSink() }
|
|
11
|
+
stats.onQueued(); stats.onSub()
|
|
12
|
+
stats.onDropSize()
|
|
13
|
+
stats.onDropBusy()
|
|
14
|
+
stats.onDropNullI420()
|
|
15
|
+
assertThat(stats.conserved()).isTrue()
|
|
16
|
+
assertThat(stats.sinkCount()).isEqualTo(4)
|
|
17
|
+
assertThat(stats.subCount()).isEqualTo(1)
|
|
18
|
+
assertThat(stats.dropCount()).isEqualTo(3)
|
|
19
|
+
assertThat(stats.inFlightCount()).isEqualTo(0)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun latencyStraddleIsOneHundredPercentOverTenTicks() {
|
|
24
|
+
var clock = 0L
|
|
25
|
+
val stats = PipelineStats { clock }
|
|
26
|
+
val ticks = ArrayList<String>()
|
|
27
|
+
stats.onSink()
|
|
28
|
+
stats.onQueued()
|
|
29
|
+
clock += 1000
|
|
30
|
+
ticks.add(stats.tick())
|
|
31
|
+
stats.onSub()
|
|
32
|
+
repeat(9) {
|
|
33
|
+
clock += 1000
|
|
34
|
+
ticks.add(stats.tick())
|
|
35
|
+
}
|
|
36
|
+
assertThat(stats.subCount()).isEqualTo(stats.sinkCount())
|
|
37
|
+
assertThat(stats.conserved()).isTrue()
|
|
38
|
+
val last = ticks.last()
|
|
39
|
+
assertThat(last).contains("cum{sink=1 sub=1 drop=0 inFlight=0}")
|
|
40
|
+
assertThat(last).doesNotContain("CONSERVE_FAIL")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun cumulativeCountersNeverDecrease() {
|
|
45
|
+
var clock = 0L
|
|
46
|
+
val stats = PipelineStats { clock }
|
|
47
|
+
stats.onSink(); stats.onQueued(); stats.onSub()
|
|
48
|
+
clock += 1000
|
|
49
|
+
val first = stats.tick()
|
|
50
|
+
clock += 1000
|
|
51
|
+
val idle = stats.tick()
|
|
52
|
+
assertThat(extractCum(first)).isEqualTo(extractCum(idle))
|
|
53
|
+
assertThat(stats.sinkCount()).isEqualTo(1)
|
|
54
|
+
assertThat(stats.subCount()).isEqualTo(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun idleTickStillEmitsSinkZero() {
|
|
59
|
+
var clock = 0L
|
|
60
|
+
val stats = PipelineStats { clock }
|
|
61
|
+
clock += 1000
|
|
62
|
+
val line = stats.tick()
|
|
63
|
+
assertThat(line).startsWith("P6 ladder")
|
|
64
|
+
assertThat(line).contains("sink=0.0")
|
|
65
|
+
assertThat(line).contains("dec=na")
|
|
66
|
+
assertThat(line).contains("wire=0x0@na kbps=na codec=na")
|
|
67
|
+
assertThat(line).contains("drop{")
|
|
68
|
+
assertThat(line).contains("ms{")
|
|
69
|
+
assertThat(line).contains("chroma{")
|
|
70
|
+
assertThat(line).contains("cum{sink=0 sub=0 drop=0 inFlight=0}")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun queuedFrameCountsAsInFlightUntilSettled() {
|
|
75
|
+
val stats = PipelineStats()
|
|
76
|
+
stats.onSink()
|
|
77
|
+
stats.onQueued()
|
|
78
|
+
assertThat(stats.conserved()).isTrue()
|
|
79
|
+
assertThat(stats.inFlightCount()).isEqualTo(1)
|
|
80
|
+
stats.onSub()
|
|
81
|
+
assertThat(stats.inFlightCount()).isEqualTo(0)
|
|
82
|
+
assertThat(stats.conserved()).isTrue()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The device logged 14 CONSERVE_FAIL lines on a pipeline with zero drops.
|
|
87
|
+
* Cause was a tracked inFlight counter read mid-settle, not lost frames.
|
|
88
|
+
*/
|
|
89
|
+
@Test
|
|
90
|
+
fun concurrentSendsNeverReportConserveFail() {
|
|
91
|
+
val stats = PipelineStats { 0L }
|
|
92
|
+
val sender = Thread {
|
|
93
|
+
repeat(20_000) {
|
|
94
|
+
stats.onSink()
|
|
95
|
+
stats.onQueued()
|
|
96
|
+
stats.onSub()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
val failures = ArrayList<String>()
|
|
100
|
+
val reader = Thread {
|
|
101
|
+
repeat(20_000) {
|
|
102
|
+
if (!stats.conserved()) failures.add("inFlight=${stats.inFlightCount()}")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sender.start(); reader.start()
|
|
106
|
+
sender.join(); reader.join()
|
|
107
|
+
assertThat(failures).isEmpty()
|
|
108
|
+
assertThat(stats.inFlightCount()).isEqualTo(0)
|
|
109
|
+
assertThat(stats.tick()).doesNotContain("CONSERVE_FAIL")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun recvHealthReportsDeltasNotAbsolutes() {
|
|
114
|
+
val stats = PipelineStats { 0L }
|
|
115
|
+
assertThat(stats.recvLabel()).isEqualTo("recv{na}")
|
|
116
|
+
stats.setRecvHealth(
|
|
117
|
+
PipelineStats.RecvHealth(assembled = 100, dropped = 5, packetsLost = 40, nack = 7, pli = 2, freezes = 1),
|
|
118
|
+
)
|
|
119
|
+
assertThat(stats.recvLabel()).contains("drop=5", "lost=40", "nack=7", "pli=2", "freeze=1")
|
|
120
|
+
stats.setRecvHealth(
|
|
121
|
+
PipelineStats.RecvHealth(assembled = 115, dropped = 5, packetsLost = 52, nack = 9, pli = 2, freezes = 3),
|
|
122
|
+
)
|
|
123
|
+
assertThat(stats.recvLabel()).contains("drop=0", "lost=12", "nack=2", "pli=0", "freeze=2")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
fun recvHealthAveragesPerFrameTimings() {
|
|
128
|
+
val stats = PipelineStats { 0L }
|
|
129
|
+
stats.setRecvHealth(
|
|
130
|
+
PipelineStats.RecvHealth(
|
|
131
|
+
assembled = 200,
|
|
132
|
+
decodeSec = 0.8,
|
|
133
|
+
jitterBufferSec = 12.0,
|
|
134
|
+
jitterBufferEmits = 200,
|
|
135
|
+
jitter = 0.015,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
val label = stats.recvLabel()
|
|
139
|
+
assertThat(label).contains("decMs=4.0")
|
|
140
|
+
assertThat(label).contains("jbMs=60.0")
|
|
141
|
+
assertThat(label).contains("jit=15.0")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Test
|
|
145
|
+
fun recvHealthToleratesMissingMembers() {
|
|
146
|
+
val stats = PipelineStats { 0L }
|
|
147
|
+
stats.setRecvHealth(PipelineStats.RecvHealth())
|
|
148
|
+
val label = stats.recvLabel()
|
|
149
|
+
assertThat(label).contains("drop=na", "lost=na", "decMs=na", "jbMs=na", "decImpl=na")
|
|
150
|
+
assertThat(stats.tick()).contains("recv{")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
fun recvHealthPrintsDecoderImplementation() {
|
|
155
|
+
val stats = PipelineStats { 0L }
|
|
156
|
+
stats.setRecvHealth(PipelineStats.RecvHealth(decImpl = "OMX.qcom.video.decoder.avc"))
|
|
157
|
+
assertThat(stats.recvLabel()).contains("decImpl=OMX.qcom.video.decoder.avc")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
fun ladderIncludesArmLabel() {
|
|
162
|
+
var clock = 0L
|
|
163
|
+
val stats = PipelineStats { clock }
|
|
164
|
+
clock += 1000
|
|
165
|
+
assertThat(stats.tick()).contains("arm=whep")
|
|
166
|
+
stats.arm = "synthetic"
|
|
167
|
+
clock += 1000
|
|
168
|
+
assertThat(stats.tick()).contains("arm=synthetic")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Test
|
|
172
|
+
fun ladderExposesCpuAttributionFields() {
|
|
173
|
+
var clock = 0L
|
|
174
|
+
val stats = PipelineStats { clock }
|
|
175
|
+
stats.toI420.record(4_000_000)
|
|
176
|
+
stats.sinkCb.record(8_000_000)
|
|
177
|
+
stats.split.record(1_000_000)
|
|
178
|
+
stats.onDestAlloc()
|
|
179
|
+
stats.onPlaneAlloc()
|
|
180
|
+
stats.onPlaneAlloc()
|
|
181
|
+
clock += 1000
|
|
182
|
+
val line = stats.tick()
|
|
183
|
+
assertThat(line).contains("i420P95=4.0")
|
|
184
|
+
assertThat(line).contains("sinkCbP95=8.0")
|
|
185
|
+
assertThat(line).contains("splitP95=1.0")
|
|
186
|
+
assertThat(line).contains("alloc{dest=1 plane=2}")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Test
|
|
190
|
+
fun ladderCarriesPathBufStrideZcAndCopyP95Na() {
|
|
191
|
+
var clock = 0L
|
|
192
|
+
val stats = PipelineStats { clock }
|
|
193
|
+
clock += 1000
|
|
194
|
+
val idle = stats.tick()
|
|
195
|
+
assertThat(idle).contains("path{mode=texture copy=packsplit pix=i420}")
|
|
196
|
+
assertThat(idle).contains("wire=0x0@na kbps=na codec=na")
|
|
197
|
+
assertThat(idle).contains("buf{tex=0 i420=0 other=0}")
|
|
198
|
+
assertThat(idle).contains("stride{y=0 u=0 v=0 tight=0 padded=0}")
|
|
199
|
+
assertThat(idle).contains("zc{on=0 used=0 fell=0 padded=0 heldMax=0 timeout=0}")
|
|
200
|
+
assertThat(idle).contains("copyP95=na")
|
|
201
|
+
|
|
202
|
+
stats.pathMode = "bytebuf"
|
|
203
|
+
stats.pathCopy = "nv12"
|
|
204
|
+
stats.pix = "nv12"
|
|
205
|
+
stats.codecName = "h264 sw"
|
|
206
|
+
stats.onFrameBuffer("tex")
|
|
207
|
+
stats.onFrameBuffer("i420")
|
|
208
|
+
stats.onFrameBuffer("i420")
|
|
209
|
+
stats.onFrameBuffer("nv12")
|
|
210
|
+
stats.onStrides(1280, 640, 640, 1280)
|
|
211
|
+
stats.onStrides(1280, 768, 768, 1280)
|
|
212
|
+
stats.zcOn = 1
|
|
213
|
+
stats.onZcUsed(1)
|
|
214
|
+
stats.onZcUsed(2)
|
|
215
|
+
stats.onZcFell()
|
|
216
|
+
stats.onZcPadded()
|
|
217
|
+
stats.onZcTimeout()
|
|
218
|
+
stats.copy.record(3_000_000)
|
|
219
|
+
clock += 1000
|
|
220
|
+
val line = stats.tick()
|
|
221
|
+
assertThat(line).contains("path{mode=bytebuf copy=nv12 pix=nv12}")
|
|
222
|
+
assertThat(line).contains("codec=h264_sw")
|
|
223
|
+
assertThat(line).contains("buf{tex=1 i420=2 other=1}")
|
|
224
|
+
assertThat(line).contains("stride{y=1280 u=768 v=768 tight=1 padded=1}")
|
|
225
|
+
assertThat(line).contains("zc{on=1 used=2 fell=1 padded=1 heldMax=2 timeout=1}")
|
|
226
|
+
assertThat(line).contains("copyP95=3.0")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Test
|
|
230
|
+
fun ladderPrintsAcsWireGeometryAndBitrate() {
|
|
231
|
+
var clock = 0L
|
|
232
|
+
val stats = PipelineStats { clock }
|
|
233
|
+
stats.wireWidth = 960
|
|
234
|
+
stats.wireHeight = 540
|
|
235
|
+
stats.wireFps = 29.8
|
|
236
|
+
stats.wireBitrateBps = 1_480_000
|
|
237
|
+
clock += 1000
|
|
238
|
+
stats.codecName = "H264SkypeEncoder_HW"
|
|
239
|
+
assertThat(stats.tick()).contains("wire=960x540@29.8 kbps=1480 codec=H264SkypeEncoder_HW")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Test
|
|
243
|
+
fun ladderExposesRecvRateSeparateFromDecode() {
|
|
244
|
+
var clock = 0L
|
|
245
|
+
val stats = PipelineStats { clock }
|
|
246
|
+
stats.recvFps = 14.8
|
|
247
|
+
stats.decodedFps = 8.1
|
|
248
|
+
clock += 1000
|
|
249
|
+
val line = stats.tick()
|
|
250
|
+
assertThat(line).contains("recv=14.8")
|
|
251
|
+
assertThat(line).contains("dec=8.1")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun extractCum(line: String): String =
|
|
255
|
+
Regex("""cum\{[^}]+\}""").find(line)?.value ?: line
|
|
256
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.telemetry
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class ProcessCpuTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun percentMatchesTopStyleOverOneSecond() {
|
|
9
|
+
assertThat(ProcessCpu.percent(cpuDeltaMs = 1120, wallDeltaMs = 1000)).isEqualTo(112.0)
|
|
10
|
+
assertThat(ProcessCpu.percent(cpuDeltaMs = 200, wallDeltaMs = 1000)).isEqualTo(20.0)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun labelFormatsNaUntilFirstSample() {
|
|
15
|
+
assertThat(ProcessCpu.label(null, 8)).isEqualTo("cpu{proc=na cores=8}")
|
|
16
|
+
assertThat(ProcessCpu.label(112.4, 8)).isEqualTo("cpu{proc=112.4 cores=8}")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.telemetry
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
import java.util.concurrent.CountDownLatch
|
|
6
|
+
import java.util.concurrent.Executors
|
|
7
|
+
import java.util.concurrent.TimeUnit
|
|
8
|
+
|
|
9
|
+
class RingPercentileTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun emptyIsNa() {
|
|
12
|
+
assertThat(RingPercentile().p50()).isEqualTo("na")
|
|
13
|
+
assertThat(RingPercentile().p95()).isEqualTo("na")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Test
|
|
17
|
+
fun knownInputs() {
|
|
18
|
+
val ring = RingPercentile(8)
|
|
19
|
+
ring.record(10_000_000)
|
|
20
|
+
ring.record(20_000_000)
|
|
21
|
+
ring.record(40_000_000)
|
|
22
|
+
assertThat(ring.p50()).isEqualTo("20.0")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun wraparoundKeepsNewest() {
|
|
27
|
+
val ring = RingPercentile(3)
|
|
28
|
+
repeat(6) { index -> ring.record((index + 1) * 1_000_000L) }
|
|
29
|
+
assertThat(ring.size()).isEqualTo(3)
|
|
30
|
+
assertThat(ring.p50()).isEqualTo("5.0")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun concurrentRecordDoesNotCorruptCount() {
|
|
35
|
+
val ring = RingPercentile(32)
|
|
36
|
+
val pool = Executors.newFixedThreadPool(4)
|
|
37
|
+
val latch = CountDownLatch(4)
|
|
38
|
+
repeat(4) {
|
|
39
|
+
pool.execute {
|
|
40
|
+
repeat(50) { ring.record(1_000_000) }
|
|
41
|
+
latch.countDown()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()
|
|
45
|
+
pool.shutdown()
|
|
46
|
+
assertThat(ring.size()).isEqualTo(32)
|
|
47
|
+
assertThat(ring.p50()).isNotEqualTo("na")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.video
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class AcsTimestampTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun streamClockWinsWhenItIsAdvancing() {
|
|
9
|
+
val ticks = AcsTimestamp.resolve(streamTicks = 5_000, captureNs = 1_000_000, lastTicks = 100)
|
|
10
|
+
assertThat(ticks).isEqualTo(5_000)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun captureNsConvertsToHundredNanosecondTicks() {
|
|
15
|
+
val ticks = AcsTimestamp.resolve(streamTicks = 0, captureNs = 66_700_000, lastTicks = 0)
|
|
16
|
+
assertThat(ticks).isEqualTo(667_000)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A run of timestamp-0 frames is what Teams renders as a freeze: the jitter
|
|
21
|
+
* buffer holds the last picture because presentation time never moves.
|
|
22
|
+
*/
|
|
23
|
+
@Test
|
|
24
|
+
fun zeroInputsStillAdvance() {
|
|
25
|
+
val first = AcsTimestamp.resolve(streamTicks = 0, captureNs = 0, lastTicks = 0)
|
|
26
|
+
val second = AcsTimestamp.resolve(streamTicks = 0, captureNs = 0, lastTicks = first)
|
|
27
|
+
assertThat(first).isEqualTo(1)
|
|
28
|
+
assertThat(second).isGreaterThan(first)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun neverReusesTheLastTick() {
|
|
33
|
+
val stuckStream = 40L
|
|
34
|
+
val ticks = AcsTimestamp.resolve(streamTicks = stuckStream, captureNs = 1_000, lastTicks = 40)
|
|
35
|
+
assertThat(ticks).isEqualTo(41)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test
|
|
39
|
+
fun captureBehindLastTickStillAdvances() {
|
|
40
|
+
val ticks = AcsTimestamp.resolve(streamTicks = 0, captureNs = 100, lastTicks = 9_000)
|
|
41
|
+
assertThat(ticks).isEqualTo(9_001)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.video
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class FrameGeometryTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun rotationZeroUsesBufferSize() {
|
|
9
|
+
val size = FrameGeometry.packSize(1280, 720, 0)
|
|
10
|
+
assertThat(size.width).isEqualTo(1280)
|
|
11
|
+
assertThat(size.height).isEqualTo(720)
|
|
12
|
+
assertThat(size.rotationNonZero).isFalse()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun rotation90KeepsBufferCoordinatesNotRotatedDisplaySize() {
|
|
17
|
+
val bufferW = 1280
|
|
18
|
+
val bufferH = 720
|
|
19
|
+
val rotatedWidth = bufferH
|
|
20
|
+
val rotatedHeight = bufferW
|
|
21
|
+
val size = FrameGeometry.packSize(bufferW, bufferH, 90)
|
|
22
|
+
assertThat(size.width).isEqualTo(bufferW)
|
|
23
|
+
assertThat(size.height).isEqualTo(bufferH)
|
|
24
|
+
assertThat(size.rotationNonZero).isTrue()
|
|
25
|
+
assertThat(size.width).isNotEqualTo(rotatedWidth)
|
|
26
|
+
assertThat(size.height).isNotEqualTo(rotatedHeight)
|
|
27
|
+
val overrun = I420Packer.packedSize(rotatedWidth, rotatedHeight)
|
|
28
|
+
val actual = I420Packer.packedSize(size.width, size.height)
|
|
29
|
+
assertThat(overrun).isEqualTo(actual)
|
|
30
|
+
val planeIfRotated = rotatedHeight * 1280
|
|
31
|
+
assertThat(planeIfRotated).isGreaterThan(bufferW * bufferH)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.video
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Asserts the advertised format arithmetic via [I420FormatSpec]. The ACS
|
|
8
|
+
* VideoStreamFormat object itself is not constructed here: it is native-backed
|
|
9
|
+
* and throws ExceptionInInitializerError outside an Android runtime.
|
|
10
|
+
*/
|
|
11
|
+
class AcsFrameSenderFormatTest {
|
|
12
|
+
@Test
|
|
13
|
+
fun i420FormatOffersTight720p20() {
|
|
14
|
+
val spec = I420FormatSpec.of()
|
|
15
|
+
assertThat(spec.width).isEqualTo(1280)
|
|
16
|
+
assertThat(spec.height).isEqualTo(720)
|
|
17
|
+
assertThat(spec.fps).isEqualTo(20f)
|
|
18
|
+
assertThat(spec.strideY).isEqualTo(1280)
|
|
19
|
+
assertThat(spec.strideU).isEqualTo(640)
|
|
20
|
+
assertThat(spec.strideV).isEqualTo(640)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
fun i420FormatRoundsOddChromaStrides() {
|
|
25
|
+
val spec = I420FormatSpec.of(width = 3, height = 3, fps = 15f)
|
|
26
|
+
assertThat(spec.strideY).isEqualTo(3)
|
|
27
|
+
assertThat(spec.strideU).isEqualTo(2)
|
|
28
|
+
assertThat(spec.strideV).isEqualTo(2)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun advertisedStridesMatchPacker() {
|
|
33
|
+
assertThat(I420Packer.chromaStride(1280)).isEqualTo(640)
|
|
34
|
+
assertThat(I420Packer.packedSize(1280, 720)).isEqualTo(1_382_400)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A disagreement here means ACS reads chroma at the wrong offset: green frames. */
|
|
38
|
+
@Test
|
|
39
|
+
fun advertisedSizeMatchesWhatThePackerWrites() {
|
|
40
|
+
for ((w, h) in listOf(1280 to 720, 960 to 540, 640 to 360, 3 to 3, 641 to 361)) {
|
|
41
|
+
val spec = I420FormatSpec.of(width = w, height = h)
|
|
42
|
+
assertThat(spec.packedSize())
|
|
43
|
+
.describedAs("%dx%d", w, h)
|
|
44
|
+
.isEqualTo(I420Packer.packedSize(w, h))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun i420FormatStaysInsideAcsBounds() {
|
|
50
|
+
assertThat(I420FormatSpec.of().withinAcsBounds()).isTrue()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
fun parseOrNullAllowsDocumentedVirtualCameraSizesOnly() {
|
|
55
|
+
assertThat(I420FormatSpec.parseOrNull(1280, 720, 15f)).isNotNull()
|
|
56
|
+
assertThat(I420FormatSpec.parseOrNull(960, 540, 30f)).isNotNull()
|
|
57
|
+
assertThat(I420FormatSpec.parseOrNull(540, 960, 30f)).isNull()
|
|
58
|
+
assertThat(I420FormatSpec.parseOrNull(854, 480, 15f)).isNull()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
fun namedResolutionMapsAcsEnumsAndRejectsNearMatches() {
|
|
63
|
+
assertThat(I420FormatSpec.of(1280, 720).namedResolution()).isEqualTo(AcsNamedResolution.P720)
|
|
64
|
+
assertThat(I420FormatSpec.of(960, 540).namedResolution()).isEqualTo(AcsNamedResolution.P540)
|
|
65
|
+
assertThat(I420FormatSpec.of(858, 480).namedResolution()).isEqualTo(AcsNamedResolution.P480)
|
|
66
|
+
assertThat(I420FormatSpec.of(640, 360).namedResolution()).isEqualTo(AcsNamedResolution.P360)
|
|
67
|
+
assertThat(I420FormatSpec.of(854, 480).namedResolution()).isNull()
|
|
68
|
+
assertThat(I420FormatSpec.of(540, 960).namedResolution()).isNull()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun outOfRangeGeometryIsReportedNotSilentlyClamped() {
|
|
73
|
+
assertThat(I420FormatSpec.of(width = 3, height = 3).withinAcsBounds()).isFalse()
|
|
74
|
+
assertThat(I420FormatSpec.of(width = 4096, height = 2160).withinAcsBounds()).isFalse()
|
|
75
|
+
assertThat(I420FormatSpec.of(fps = 60f).withinAcsBounds()).isFalse()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.video
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
import java.nio.ByteBuffer
|
|
6
|
+
|
|
7
|
+
class I420PackerTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun percentileMsHandlesEmptyAndOrder() {
|
|
10
|
+
assertThat(I420Packer.percentileMs(LongArray(0), 0.50)).isEqualTo("na")
|
|
11
|
+
assertThat(I420Packer.percentileMs(longArrayOf(10_000_000, 20_000_000, 40_000_000), 0.50)).isEqualTo("20.0")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
fun packedSizeUsesChromaRounding() {
|
|
16
|
+
assertThat(I420Packer.packedSize(4, 4)).isEqualTo(24)
|
|
17
|
+
assertThat(I420Packer.packedSize(3, 3)).isEqualTo(3 * 3 + 2 * 2 * 2)
|
|
18
|
+
assertThat(I420Packer.packedSize(1280, 720)).isEqualTo(1280 * 720 * 3 / 2)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
fun planeMinBytesIsLastRowPlusFullStrides() {
|
|
23
|
+
assertThat(I420Packer.planeMinBytes(8, 8, 4)).isEqualTo(32)
|
|
24
|
+
assertThat(I420Packer.planeMinBytes(16, 8, 4)).isEqualTo(16 * 3 + 8)
|
|
25
|
+
assertThat(I420Packer.planeMinBytes(4, 8, 4)).isEqualTo(0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
fun copyPlaneFromPaddedStrideWritesTightDest() {
|
|
30
|
+
val width = 4
|
|
31
|
+
val height = 4
|
|
32
|
+
val padded = buildPlanes(width, height, strideY = 8, strideUv = 4, seed = 7)
|
|
33
|
+
val dest = ByteBuffer.allocate(width * height)
|
|
34
|
+
I420Packer.copyPlane(padded.y, 8, width, height, dest)
|
|
35
|
+
dest.flip()
|
|
36
|
+
val out = ByteArray(dest.remaining())
|
|
37
|
+
dest.get(out)
|
|
38
|
+
assertThat(out).containsExactly(
|
|
39
|
+
7, 8, 9, 10,
|
|
40
|
+
11, 12, 13, 14,
|
|
41
|
+
15, 16, 17, 18,
|
|
42
|
+
19, 20, 21, 22,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun chromaStrideMatchesAcsI420Offer() {
|
|
48
|
+
assertThat(I420Packer.chromaStride(1280)).isEqualTo(640)
|
|
49
|
+
assertThat(I420Packer.chromaStride(3)).isEqualTo(2)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun paddedStridesPackIdenticallyToTightStrides() {
|
|
54
|
+
val width = 4
|
|
55
|
+
val height = 4
|
|
56
|
+
val tight = buildPlanes(width, height, strideY = 4, strideUv = 2, seed = 7)
|
|
57
|
+
val padded = buildPlanes(width, height, strideY = 8, strideUv = 4, seed = 7)
|
|
58
|
+
val tightOut = ByteBuffer.allocate(I420Packer.packedSize(width, height))
|
|
59
|
+
val paddedOut = ByteBuffer.allocate(I420Packer.packedSize(width, height))
|
|
60
|
+
I420Packer.pack(
|
|
61
|
+
tight.y, 4, tight.u, 2, tight.v, 2, width, height, tightOut,
|
|
62
|
+
)
|
|
63
|
+
I420Packer.pack(
|
|
64
|
+
padded.y, 8, padded.u, 4, padded.v, 4, width, height, paddedOut,
|
|
65
|
+
)
|
|
66
|
+
assertThat(bytes(paddedOut)).isEqualTo(bytes(tightOut))
|
|
67
|
+
assertThat(tightOut.remaining()).isEqualTo(I420Packer.packedSize(width, height))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
fun oddDimensionsPackExactBytes() {
|
|
72
|
+
val width = 3
|
|
73
|
+
val height = 3
|
|
74
|
+
val planes = buildPlanes(width, height, strideY = 5, strideUv = 3, seed = 11)
|
|
75
|
+
val dest = ByteBuffer.allocate(I420Packer.packedSize(width, height))
|
|
76
|
+
I420Packer.pack(planes.y, 5, planes.u, 3, planes.v, 3, width, height, dest)
|
|
77
|
+
val packed = bytes(dest)
|
|
78
|
+
assertThat(packed).hasSize(3 * 3 + 2 * 2 * 2)
|
|
79
|
+
// Visible Y is seed + row*width + col; chroma is 2x2 after (3+1)/2 rounding.
|
|
80
|
+
assertThat(packed.copyOfRange(0, 9)).containsExactly(
|
|
81
|
+
11, 12, 13,
|
|
82
|
+
14, 15, 16,
|
|
83
|
+
17, 18, 19,
|
|
84
|
+
)
|
|
85
|
+
assertThat(packed.copyOfRange(9, 13)).containsExactly(31, 32, 33, 34)
|
|
86
|
+
assertThat(packed.copyOfRange(13, 17)).containsExactly(41, 42, 43, 44)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun buildPlanes(
|
|
90
|
+
width: Int,
|
|
91
|
+
height: Int,
|
|
92
|
+
strideY: Int,
|
|
93
|
+
strideUv: Int,
|
|
94
|
+
seed: Int,
|
|
95
|
+
): Planes {
|
|
96
|
+
val chromaW = (width + 1) / 2
|
|
97
|
+
val chromaH = (height + 1) / 2
|
|
98
|
+
val y = ByteBuffer.allocate(strideY * height)
|
|
99
|
+
val u = ByteBuffer.allocate(strideUv * chromaH)
|
|
100
|
+
val v = ByteBuffer.allocate(strideUv * chromaH)
|
|
101
|
+
for (row in 0 until height) {
|
|
102
|
+
for (col in 0 until width) {
|
|
103
|
+
y.put(row * strideY + col, (seed + row * width + col).toByte())
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (row in 0 until chromaH) {
|
|
107
|
+
for (col in 0 until chromaW) {
|
|
108
|
+
u.put(row * strideUv + col, (seed + 20 + row * chromaW + col).toByte())
|
|
109
|
+
v.put(row * strideUv + col, (seed + 30 + row * chromaW + col).toByte())
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
y.rewind()
|
|
113
|
+
u.rewind()
|
|
114
|
+
v.rewind()
|
|
115
|
+
return Planes(y, u, v)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun bytes(buffer: ByteBuffer): ByteArray {
|
|
119
|
+
val copy = buffer.duplicate()
|
|
120
|
+
val out = ByteArray(copy.remaining())
|
|
121
|
+
copy.get(out)
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private data class Planes(val y: ByteBuffer, val u: ByteBuffer, val v: ByteBuffer)
|
|
126
|
+
}
|