A Two Part Golang System for Viewer Interaction During a Livestream

August 12, 2020

The Premise

For my recent live performance as part of Bonus Stage Vancouver’s Virtual VGM Fest 5.0, I had a sudden idea that came to me the night before the show for a piece of audience interactivity. My stage name is Katamari, which is the name of the titular roll-everything-up ball from the video game Katamari Damacy and the series that followed it. A little green character known as The Prince pushes a Katamari around and rolls everything up. My idea was this: a simple web page that viewers could visit, type in a word or two, and make an object appear on my screen with that text, while a katamari ball sweeps over the screen on an interval, rolling up all in its path. Here’s the final product before I go over how it was done:

Animation of the program in motion

The overall project consists of two separate binaries that perform different duties.

The server:

The client:

Both parts of this project were created with Golang, which has quickly become my favorite language to work it. It’s so easy to set everything up, and you will see how versatile it is as I make both a web server and a game client! The source code for both is available on my GitHub for those interested: Server and Client

The Client

(Note: This section does not contain much code, as the combination of both updating & rendering makes things a bit more complex. I instead opted to describe the evolution of the client while referencing some important methods.)

I had a feeling that the server would be much easier to implement, as I have a quite a bit of experience dealing with HTTP servers in Go. However, the client needed to be a simple game with a GUI. I had previously checked out ebiten, a game library for Go, so I hit up their docs and went from there. I was trying to make this quickly, not nicely, so the code ended up with lots of hardcoded values and incrementing counters acting as timers. Definitely room for improvement there, as any kind of slowdown in the game would affect the timing of those values, but it worked for what I was doing. Eventually, I had successfully imported some GIFs and made them animate across the screen! The katamari just goes off-screen and then wraps around at a certain point, it is always continually moving, but being off-screen gives the illusion of starting and stopping.

Next was spawning in the objects (cows) that would be rolled up. Originally I wanted the objects to be static until the ball rolled by them, at which point they would “stick” to the ball and roll off with it, but I didn’t have the time to implement this, so they just disappear behind the ball instead and are removed. These objects are just represented by a simple struct with a text element (taken from the tentsuyu helper library for ebiten) and an x/y coordinate. I put all the code to add in a new object under this SpawnThing(name string) method, and while testing I just hooked this up to spacebar. This will later be used whenever the network call receives a response that requires an object to be spawned. Tweak some values to watch for collision of the objects and the ball, and boom, the client is fully functional! Just to add some network calls to make it run on its own.

I think one of the best features of Golang is how easy it is to marshal and unmarshal from JSON, which makes network programming a breeze. For this case, I declared a RequestType which contained a slice of strings, and was able to read the response body from our GET request directly into it. As long as no errors happened, we were good to go and could iterate over this slice of strings, spawning objects using the SpawnThing method just as we did before. Easy! The client is finished! On to the server!

The Server

This part will be easy! For this, I’m using Gorilla Mux and Gorilla Handlers to take care of routing and CORS, respectively. We only have two routes on the server, a POST route that the users will send data to, and a GET route which our client will use to retrieve that data from. I wanted the paths to be different so that I didn’t have any users accidentally browse to a URL and perform a GET when they were supposed to do a POST, so I made GET be on “/drain” (because we drain the queue) and POST be on “/".

// GetRouter returns the route tree.
func GetRouter() *mux.Router {
  r := mux.NewRouter().StrictSlash(true)

  r.Use(handlers.RecoveryHandler())

  r.HandleFunc("/drain", Get).Methods("GET")
  r.HandleFunc("/", Post).Methods("POST")

  return r
}

When users POST data, we simply decode the request body into a request type like we did for the client, and add the string to our queue of strings. There is totally an opportunity here for someone to maliciously insert a huge string, which could consume a lot of memory if exploited. It would be easy to limit the accepted request body size, and also to just keep a map of IPs that requests came from and check that list before allowing the request, a cheap rate limiting solution.

// Post will handle a post request containing a name to save in the queue.
func Post(w http.ResponseWriter, r *http.Request) {
    var req request
    err := json.NewDecoder(r.Body).Decode(&req)
    if err != nil {
        fmt.Println(err)
        w.WriteHeader(500)
        return
    }

    qLock.Lock()
    queue = append(queue, req.Name)
    qLock.Unlock()

    w.WriteHeader(200)
}

When the client sends a request to the GET endpoint, we simply respond with the contents of the string queue (as a JSON string) and then empty it.

// Get will handle an get request to server the contents of the queue and reset it.
func Get(w http.ResponseWriter, r *http.Request) {
    qLock.Lock()

    response := struct {
        Names []string
    }{}

    response.Names = queue

    b, err := json.Marshal(response)
    if err != nil {
        panic(err)
    }

    _, err = w.Write(b)
    if err != nil {
        panic(err)
    }

    //empty the queue
    queue = []string{}

    qLock.Unlock()
}

Reflection

Things I would update/change:

To see this in action, you can view the entire concert over on YouTube!

References