Synchronised jukebox with Vert.x and Kotlin

With Covid-19 keeping everyone inside, hopefully, I was thinking of web apps that are a bit more collaborative in nature. General directions I was thinking of taking involved making something similar to a friend group quiz application like Kahoot, or a Google docs style editor but for your GitHub repos, or a jukebox where all clients are listening to the same audio at roughly the same position. By the title of this post, it’s probably obvious which one I went with.

We’ll be looking at the server-side code for this, which is written in Kotlin and Vert.x.

The application can be decomposed into three main things it needs to do:

  • Let clients connect by pointing to an endpoint
  • Keep track of connected clients
  • Periodically send a chunk of the audio data to connected clients

Let’s look at each one by one.

We can keep track of all clients by keeping a

val streamers = mutableSetOf<HttpServerResponse>()

And for a client to connect they simply connect to a handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// some setup
val vertx = Vertx.vertx()
val router = Router.router(vertx)

router.route("/stream").handler {
streamers.add(it.response())

it.response()
.putHeader("Content-Type", "audio/mpeg")
.setChunked(true)

// add clean up handler to remove disconnected client
it.response().endHandler { _ -> streamers.remove(it.response()) }
}

To periodically send a chunk of the audio we can use vertx’s set periodic function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Server side state
val streamers = mutableSetOf<HttpServerResponse>()
var positionInFile = 0L
var playingNow = vertx.fileSystem().openBlocking("filename.mp3", OpenOptions().setRead(true))

// every 100ms do
vertx.setPeriodic(100) { _ ->
// Don't send the frame if no one is connected i.e. pause
if (streamers.isEmpty()) return@setPeriodic
playingNow.read(Buffer.buffer(4096), 0, positionInFile, 4096) { asyncResult ->
val buffer = asyncResult.result()
// if (buffer.length() == 0) -> switch over to next file in playlist
positionInFile += buffer.length()
streamers.filter { !it.writeQueueFull() }.forEach { it.write(buffer.copy()) }
}
}

Run the server with vertx.createHttpServer().requestHandler(router).listen(8080)

Navigate to localhost:8080/stream in Chrome/Safari or using VLC using File -> open network -> URL: http://localhost:8080/stream

And you should start hearing the stream after some initial buffering. After a little while open another tab pointed to the same URL and you should hear the audio being played at roughly the same position.

Modern Web development on the JVM has changed dramatically. Give a JVM microframework a try over Rails or Node.js for your next web app.

Try experimenting with the code given. Use router.post("/votes").handler { } to add functionality for users to vote on the next track to play.