mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
fix touch on head units with a second screen
LIVI lists an empty input for its cluster display before the real touchscreen, so every tap was ignored. Pick the video and input that belong to the main screen.
This commit is contained in:
1 parent
1341b4a08a
commit
bc0d6302d3
3 files changed
+81
-8
No files matched your search
@@ -383,7 +383,7 @@ class GearslipRunner(
|
||||
videoChannelId = video.serviceId
|
||||
videoConfigs = video.configs
|
||||
|
||||
log.i("video service: channel=${video.serviceId} codec=${video.codecName}")
|
||||
log.i("video service: channel=${video.serviceId} codec=${video.codecName} display=${video.displayId}")
|
||||
video.configs.forEachIndexed { index, config ->
|
||||
log.i(" config[$index] = $config")
|
||||
}
|
||||
@@ -395,7 +395,7 @@ class GearslipRunner(
|
||||
startAudioChannel(serviceDiscoveryResponse)
|
||||
startSensorChannel(serviceDiscoveryResponse)
|
||||
|
||||
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse)
|
||||
val input = ServiceDiscovery.findInputService(serviceDiscoveryResponse, video.displayId)
|
||||
if (input == null) {
|
||||
log.w("no input service advertised - projection will be output-only")
|
||||
} else {
|
||||
|
||||
@@ -51,7 +51,8 @@ object ServiceDiscovery {
|
||||
"$resolutionName @ $frameRateName, density=$density, margin=${widthMargin}x$heightMargin"
|
||||
}
|
||||
|
||||
class VideoService(val serviceId: Int, val codecType: Int, val configs: List<VideoConfig>) {
|
||||
/** [displayId] is MediaSinkService field 6: 0, the default, is the main screen. */
|
||||
class VideoService(val serviceId: Int, val codecType: Int, val configs: List<VideoConfig>, val displayId: Int = 0) {
|
||||
val codecName: String get() = CODECS[codecType] ?: "unknown($codecType)"
|
||||
}
|
||||
|
||||
@@ -131,9 +132,23 @@ object ServiceDiscovery {
|
||||
val touchWidth: Int,
|
||||
val touchHeight: Int,
|
||||
val keycodesSupported: List<Int> = emptyList(),
|
||||
/** InputSourceService field 5: which screen this input belongs to, 0 for the main one. */
|
||||
val displayId: Int = 0,
|
||||
)
|
||||
|
||||
fun findInputService(response: ByteArray): InputService? {
|
||||
/**
|
||||
* The input for the screen Gearslip projects to ([displayId]). A head unit with two screens
|
||||
* offers an input service for each: LIVI lists an empty one for its second screen first,
|
||||
* and taking that one left every touch on the real touchscreen ignored.
|
||||
*/
|
||||
fun findInputService(response: ByteArray, displayId: Int = 0): InputService? {
|
||||
val inputs = inputServices(response)
|
||||
return inputs.firstOrNull { it.displayId == displayId }
|
||||
?: inputs.firstOrNull { it.touchWidth > 0 && it.touchHeight > 0 }
|
||||
?: inputs.firstOrNull()
|
||||
}
|
||||
|
||||
private fun inputServices(response: ByteArray): List<InputService> = buildList {
|
||||
for (channel in Wire.allBytes(Wire.fields(response), 1)) {
|
||||
val service = Wire.fields(channel)
|
||||
val id = Wire.varint(service, 1)?.toInt() ?: continue
|
||||
@@ -147,9 +162,8 @@ object ServiceDiscovery {
|
||||
val keycodes = inputFields.filter { it.number == 1 }.flatMap { f ->
|
||||
f.bytes?.let(::packedVarints) ?: listOf(f.varint.toInt())
|
||||
}
|
||||
return InputService(id, width, height, keycodes)
|
||||
add(InputService(id, width, height, keycodes, Wire.varint(inputFields, 5)?.toInt() ?: 0))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Decodes a `[packed=true]` repeated varint field's raw bytes into individual values. */
|
||||
@@ -169,7 +183,16 @@ object ServiceDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main screen's video sink. A head unit with a second screen, such as LIVI with a
|
||||
* cluster display, offers a sink for each; the one without a display id (0) is the main one.
|
||||
*/
|
||||
fun findVideoService(response: ByteArray): VideoService? {
|
||||
val sinks = videoServices(response)
|
||||
return sinks.firstOrNull { it.displayId == 0 } ?: sinks.firstOrNull()
|
||||
}
|
||||
|
||||
private fun videoServices(response: ByteArray): List<VideoService> = buildList {
|
||||
for (channel in Wire.allBytes(Wire.fields(response), 1)) {
|
||||
val service = Wire.fields(channel)
|
||||
val id = Wire.varint(service, 1)?.toInt() ?: continue
|
||||
@@ -190,9 +213,8 @@ object ServiceDiscovery {
|
||||
heightMargin = Wire.varint(f, 4)?.toInt() ?: 0,
|
||||
)
|
||||
}
|
||||
return VideoService(id, codecType, configs)
|
||||
add(VideoService(id, codecType, configs, Wire.varint(sinkFields, 6)?.toInt() ?: 0))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package app.seb3thehacker.gearslip
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* LIVI 2.0.1 offers a second screen alongside the main one, and lists that screen's input first:
|
||||
* channel 3: video sink, no display id (the main screen)
|
||||
* channel 19: video sink, display_id = 1, display_type = 1 (a cluster)
|
||||
* channel 20: input service with only display_id = 1
|
||||
* channel 8: input service with a 1280x720 touchscreen and keys (the main screen)
|
||||
* Taking the first input service left every touch on the main screen ignored.
|
||||
*/
|
||||
class ServiceDiscoveryDisplayTest {
|
||||
|
||||
private fun varintField(field: Int, value: Long) = Protobuf.varintField(field, value)
|
||||
|
||||
private fun lenField(field: Int, body: ByteArray): ByteArray =
|
||||
Protobuf.varint(((field shl 3) or 2).toLong()) + Protobuf.varint(body.size.toLong()) + body
|
||||
|
||||
private fun videoSink(id: Int, displayId: Int?): ByteArray {
|
||||
val config = varintField(1, 2L) + varintField(2, 1L) + varintField(5, 180L)
|
||||
var sink = varintField(1, 3L) + lenField(4, config)
|
||||
if (displayId != null) sink += varintField(6, displayId.toLong()) + varintField(7, 1L)
|
||||
return lenField(1, varintField(1, id.toLong()) + lenField(3, sink))
|
||||
}
|
||||
|
||||
private fun input(id: Int, body: ByteArray) = lenField(1, varintField(1, id.toLong()) + lenField(4, body))
|
||||
|
||||
private fun liviResponse(): ByteArray =
|
||||
videoSink(3, null) +
|
||||
videoSink(19, 1) +
|
||||
input(20, varintField(5, 1L)) +
|
||||
input(8, lenField(1, Protobuf.varint(3L) + Protobuf.varint(4L)) + lenField(2, varintField(1, 1280L) + varintField(2, 720L)))
|
||||
|
||||
@Test
|
||||
fun picksTheMainScreensVideo() {
|
||||
val video = ServiceDiscovery.findVideoService(liviResponse())!!
|
||||
assertEquals(3, video.serviceId)
|
||||
assertEquals(0, video.displayId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun picksTheInputForTheProjectedScreen() {
|
||||
val input = ServiceDiscovery.findInputService(liviResponse(), displayId = 0)!!
|
||||
assertEquals(8, input.serviceId)
|
||||
assertEquals(1280, input.touchWidth)
|
||||
assertEquals(720, input.touchHeight)
|
||||
assertEquals(listOf(3, 4), input.keycodesSupported)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user