diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 02:29:27 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 02:29:27 +0200 |
| commit | ee9798fe3b88face2fa34f14874770f509c788a5 (patch) | |
| tree | f63e9506882863a885a907796eb977ec69137718 /packages/meshbay-android/app/src/test/kotlin | |
| parent | 243bcf017b4957237b3ddf556bd0a37296eb8798 (diff) | |
| download | meshbay-ee9798fe3b88face2fa34f14874770f509c788a5.tar.gz | |
feat(cast): music on the TV, the music bar as its remote
A cast button in Music's toolbar and in the music bar. With a television
chosen, each decrypted track goes to the relay with its cover — found as the
album card finds it — and plays there as music with its title, artist and
album; the bar's play, pause, seek, previous and next drive the receiver, its
clock is the receiver's, and the end of a track there moves the queue on.
A film or a photo taking the television pauses the bar; stopping the cast
carries the track on locally.
Photos and tracks now share one path: a whole file sent to the relay in
pieces (binary frames on Android, written to disk there), served at /file
with byte ranges and its cover at /cover, and loaded as what the relay says
it is. cast:image is gone; cast:chromecast:seek is new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-android/app/src/test/kotlin')
| -rw-r--r-- | packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastRelayTest.kt | 68 |
1 files changed, 59 insertions, 9 deletions
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastRelayTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastRelayTest.kt index 8013e29..14f49cf 100644 --- a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastRelayTest.kt +++ b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastRelayTest.kt @@ -158,9 +158,38 @@ class CastRelayTest { assertEquals(200, get(r.getString("url"), readBytes = 0).status) } - @Test fun `a photo starts the relay and is served behind the token, re-addressed when it changes`() { + private fun send(bytes: ByteArray, type: String, meta: JSONObject = JSONObject(), + cover: ByteArray? = null): String { + relay.beginFile(type, bytes.size.toLong(), cover, cover?.let { "image/jpeg" }, meta) + var at = 0 + while (at < bytes.size) { val n = minOf(3, bytes.size - at); relay.writeFile(bytes, at, n); at += n } + return relay.endFile().getString("url") + } + + private fun getRange(url: String, range: String): Reply { + val u = java.net.URI(url) + Socket(u.host, u.port).use { s -> + s.soTimeout = 5000 + s.getOutputStream().write("GET ${u.rawPath}?${u.rawQuery} HTTP/1.1\r\nHost: x\r\nRange: $range\r\n\r\n".toByteArray()) + val input = DataInputStream(s.getInputStream()) + val lines = ArrayList<String>() + val line = StringBuilder() + while (true) { + val c = input.read() + if (c == -1) break + if (c == '\n'.code) { val l = line.toString().trimEnd('\r'); if (l.isEmpty()) break; lines.add(l); line.clear() } + else line.append(c.toChar()) + } + val headers = lines.drop(1).associate { it.substringBefore(':').lowercase() to it.substringAfter(':').trim() } + val body = ByteArray(headers["content-length"]?.toInt() ?: 0) + input.readFully(body) + return Reply(lines[0].split(' ')[1].toInt(), headers, body) + } + } + + @Test fun `a file starts the relay and is served behind the token, re-addressed when it changes`() { val jpeg = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 1, 2, 3, 0xFF.toByte(), 0xD9.toByte()) - val first = relay.showImage(jpeg, "image/jpeg").getString("url") + val first = send(jpeg, "image/jpeg") assertTrue(relay.active) val reply = get(first) assertEquals(200, reply.status) @@ -169,21 +198,42 @@ class CastRelayTest { assertArrayEquals(jpeg, reply.body) assertEquals(403, get(first.replace(Regex("t=[0-9a-f]+"), "t=x")).status) // A receiver handed the same address twice shows what it already has. - val second = relay.showImage(byteArrayOf(9), "image/jpeg").getString("url") + val second = send(byteArrayOf(9), "image/jpeg") assertNotEquals(first, second) assertArrayEquals(byteArrayOf(9), get(second).body) } - @Test fun `only a picture is served as one`() { - val refused = try { relay.showImage(byteArrayOf(1), "text/html"); false } catch (e: IllegalArgumentException) { true } + @Test fun `a track is served with its cover, and by range`() { + val track = ByteArray(20) { it.toByte() } + val cover = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 7) + val url = send(track, "audio/flac", JSONObject().put("title", "A Song").put("artist", "Some Band"), cover) + assertEquals("audio/flac", relay.fileType) + val meta = relay.fileMeta()!! + assertEquals("A Song", meta.getString("title")) + assertArrayEquals(cover, get(meta.getString("cover")).body) + val part = getRange(url, "bytes=5-9") + assertEquals(206, part.status) + assertEquals("bytes 5-9/20", part.headers["content-range"]) + assertArrayEquals(track.copyOfRange(5, 10), part.body) + assertArrayEquals(track.copyOfRange(15, 20), getRange(url, "bytes=15-").body) + assertEquals(416, getRange(url, "bytes=50-60").status) + } + + @Test fun `only a whole picture or sound is served as one`() { + val refused = try { relay.beginFile("text/html", 1, null, null, JSONObject()); false } catch (e: IllegalArgumentException) { true } assertTrue(refused) - assertNull(relay.imageUrl) + assertNull(relay.fileUrl) + relay.beginFile("audio/mpeg", 10, null, null, JSONObject()) + relay.writeFile(byteArrayOf(1, 2, 3)) + val short = try { relay.endFile(); false } catch (e: IllegalStateException) { true } + assertTrue(short) + assertNull(relay.fileUrl) } - @Test fun `a film started over a photo takes the photo down`() { - relay.showImage(byteArrayOf(1), "image/jpeg") + @Test fun `a film started over a file takes the file down`() { + send(byteArrayOf(1), "image/jpeg") relay.start(null, null) - assertNull(relay.imageUrl) + assertNull(relay.fileUrl) } @Test fun `the preflight is answered before the token is checked`() { |