Commit 645849c2 authored by Simon Kirsten's avatar Simon Kirsten
Browse files

Merge branch 'tls-support-namespaces-monitoring' into 'master'

Added TLS support, namespaces and monitoring

See merge request !3
parents 4470a41a 8aac8104
Pipeline #43965 passed with stages
in 4 minutes and 10 seconds
......@@ -2,7 +2,7 @@
stage: build
image: golang:1.13
image: golang:1.14
- go mod download
# Changelog
### v1.0.0 <small>_June 20, 2020_</small>
* Server: Added TLS support
* Server: Added namespace support
* Server: Added monitoring (prometheus metric exporter)
* Display: Improved player
- Made players clickable to unmute and acknowledge mature audiences warning.
- Fixed chat sometimes showing when it is not supposed to be
- Minor fixes
### v0.9.15 <small>_June 16, 2020_</small>
* Server: Fixed chat
* Server: Fixed `volume=0` not working
......@@ -15,11 +15,15 @@ import (
const docURL = ""
func main() {
app := cli.NewApp()
var port int
var flagBrowser bool
var flagLocal bool
var tlsport int
var tlsCert, tlsKey string
var listenAddr string
var flagUseHostNamespace bool
var flagHelp bool
app.Usage = docURL
......@@ -29,20 +33,37 @@ func main() {
app.Flags = []cli.Flag{
Name: "port, p",
Name: "port",
Value: 8080,
Usage: "http port to listen on",
Destination: &port,
Name: "browser, b",
Usage: "automatically open the default browser",
Destination: &flagBrowser,
Name: "tlsport",
Value: 8443,
Usage: "https port to listen on",
Destination: &tlsport,
Name: "listen",
Value: "",
Usage: "address to listen on",
Destination: &listenAddr,
Name: "tlscert",
Usage: "TLS cert",
Destination: &tlsCert,
Name: "tlskey",
Usage: "TLS key",
Destination: &tlsKey,
Name: "local, l",
Usage: "only listen on (see doc)",
Destination: &flagLocal,
Name: "use-host-namespace",
Usage: "use the Host header as namespace",
Destination: &flagUseHostNamespace,
Name: "help, h",
......@@ -61,36 +82,56 @@ func main() {
return fmt.Errorf("Unknown arguments: %v", c.Args())
localhost := fmt.Sprintf("", port)
all := fmt.Sprintf("", port)
outbound := localhost
withTLS := tlsCert != "" && tlsKey != ""
fmt.Print("Serving on\n")
if listenAddr == "" {
fmt.Printf(" http://%s:%d\n", "localhost", port)
if withTLS {
fmt.Printf(" https://%s:%d\n", "localhost", tlsport)
if outboundIP, err := util.GetOutboundIP(); err == nil {
outbound = fmt.Sprintf("%s:%d", outboundIP, port)
fmt.Printf(" http://%s:%d\n", outboundIP, port)
if withTLS {
fmt.Printf(" https://%s:%d\n", outboundIP, tlsport)
} else {
fmt.Printf(" http://%s:%d\n", listenAddr, port)
if withTLS {
fmt.Printf(" https://%s:%d\n", listenAddr, tlsport)
var listenAddr string
if flagLocal {
listenAddr = localhost
fmt.Printf("Read the quickstart at %s/quickstart to get started.\n", docURL)
fmt.Printf("Stop with Ctrl-C or close this terminal.\n")
fmt.Printf("Serving on http://%s\n", localhost)
} else {
listenAddr = all
router := server.GetRouter(flagUseHostNamespace)
fmt.Printf("Serving on\n http://%s\n http://%s\n", localhost, outbound)
for {
errs := make(chan error)
go func() {
if err := server.ServeHTTP(fmt.Sprintf("%s:%d", listenAddr, port), router); err != nil {
errs <- err
if flagBrowser {
err := util.OpenBrowser(fmt.Sprintf("http://%s", localhost))
if err != nil {
fmt.Printf("Could not automatically open browser: %v\n", err)
if withTLS {
go func() {
if err := server.ServeHTTPS(fmt.Sprintf("%s:%d", listenAddr, tlsport), router, tlsCert, tlsKey); err != nil {
errs <- err
fmt.Printf("Read the quickstart at %s/quickstart to get started.\n", docURL)
fmt.Printf("Stop with Ctrl-C or close this terminal.\n")
select {
case err := <-errs:
return err
return server.ListenAndServe(listenAddr)
err := app.Run(os.Args)
......@@ -59,6 +59,5 @@ Darwin (macOS) | 64 bit | [stream-server](/bin/~~~${VERSION}~~~/darwin-x86_64/s
* listen tcp bind: address already in use
> Something is already using the port. This could be another Stream Server instance or some other application (like a webserver).
> Close / disable the other service or change the port via the [command-line options](
......@@ -17,24 +17,35 @@ USAGE:
stream-server [global options] [arguments...]
--port value, -p value http port to listen on (default: 8080)
--browser, -b automatically open the default browser
--local, -l only listen on (see doc)
--port value http port to listen on (default: 8080)
--tlsport value https port to listen on (default: 8443)
--listen value address to listen on (default: "")
--tlscert value TLS cert
--tlskey value TLS key
--use-host-namespace use the Host header as namespace
--help, -h show help
--version, -v print the version
By default stream-server listens on all ip addresses on port 8080. This means that it is exposed to the local network (LAN / WLAN) which is needed if the app is on a phone. Note that the phone must be in the same network as the computer / laptop.
When using the Android Emulator on the same device as the server this exposure is not necessary. Use the `--local` flag to only listen on `` aka `localhost`. The emulator must then connect to `` as described here:
When using the Android Emulator on the same device as the server this exposure is not necessary. Use the `--listen` flag to only listen on `` aka `localhost`. The emulator must then connect to `` as described here:
> Also note that the address on your development machine corresponds to the emulator's own loopback interface. If you want to access services running on your development machine loopback interface (a.k.a. on your machine), you should use the special address instead.
> [Android Studio - User guide](
## Advanced configuration
- HTTPS can be enable by providing a TLS cert and key. If both config values are present a additional HTTPS server is spawned on the tlsport.
- By default the stream-server has only one namespace, meaning that all incoming requests reference the same display.
With `--use-host-namespace` the _Host_ header is used as a namespace. This means that each different host has a different display.
This can be used to dynamically host different displays on one stream-server instance.
New displays are created on demand.
## Environment Variables
The `TWITCH_CLIENT_ID` environment variable can be set. This is only necessary during development to access the Twitch API. All official release builds ship with a default client id but you can still supply your own if you want.
......@@ -5,8 +5,8 @@ $ ./stream-server
Serving on
Read the quickstart at to get started.
Stop with Ctrl-C or close this terminal.
......@@ -15,10 +15,10 @@ Stop with Ctrl-C or close this terminal.
In the examples of this documentation only the local address is used. If you want to connect from your phone or the emulator you need to use the network address (`` in this case). Also we assume you use the default port `8080`.
1. Open <> in a browser. Leave this tab / window open while you do the other steps.
1. Open <http://localhost:8080> in a browser. Leave this tab / window open while you do the other steps.
2. Get the current state of the display:
``` java
StreamServerClient client = new StreamServerClient("");
......@@ -38,7 +38,7 @@ Stop with Ctrl-C or close this terminal.
3. Find some stream on twitch:
``` java
List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null); // no filters
......@@ -66,9 +66,9 @@ Stop with Ctrl-C or close this terminal.
4. Start playback of your selected stream with chat and muted volume:
<pre><code><{channel name here}&volume=0&show_chat=true></code></pre>
``` java
client.display("channel name here", null, 0.0f, null, true);
client.display("CHANNEL-NAME", null, 0.0f, null, true);
!!! example
......@@ -21,14 +21,14 @@ Returns [`DisplayState`](#displaystate).
??? example "Examples"
* Only set the large_channel to riotgames
``` java
/* JSONObject newState = */ client.display("riotgames", null, null, null, null);
* Set the large_channel to monstercat, disable the small_channel, set the volume to 0.8 and hide the chat.
Notice the small_scale is not changed (because of its absence)
``` java
/* JSONObject newState = */ client.display("monstercat", "", 0.8f, null, false);
......@@ -47,7 +47,7 @@ Returns [`Game`](#game) array.
??? example "Examples"
* Get top games
``` java
List<JSONObject> topGames = client.twitchGetTopGames();
......@@ -67,13 +67,13 @@ Returns [`Game`](#game) array.
??? example "Examples"
* Search for the "game" Talk Shows & Podcasts
``` java
List<JSONObject> foundGames = client.twitchSearchGames("talk show");
* Search for ove (will show games starting with "ove" like Overwatch)
``` java
List<JSONObject> foundGames = client.twitchSearchGames("ove");
......@@ -99,25 +99,25 @@ Returns [`Stream`](#stream) array.
??? example "Examples"
* Get the unfiltered top streams
``` java
List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null);
* German top streams
``` java
List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, "de");
* English top streams in Talk Shows and Podcasts
``` java
List<JSONObject> topStreams = client.twitchGetTopStreams(null, "Talk Shows and Podcasts", "en");
* Tip: If your app implements a favorite list you can very easily query these channels and see who is streaming and other information
``` java
String[] favoriteChannels = {"xqcow", "dafran", "kitboga"};
......@@ -136,7 +136,7 @@ Returns [`Stream`](#stream) array.
??? example "Examples"
* Get featured streams
``` java
List<JSONObject> featuredStreams = client.twitchGetFeaturedStreams();
......@@ -4,9 +4,12 @@ go 1.13
require ( v0.3.0 v0.2.1 v1.4.9 // indirect v4.0.2+incompatible v1.0.0 v0.12.8 v0.9.1 // indirect v1.7.0 v1.21.0 v1.0.0-20190902080502-41f04d3bba15 // indirect
) v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= v0.3.0 h1:1CABzcQydehMc54Vypki6b5+/WMmGZVWi7owRvDKo7g= v0.3.0/go.mod h1:i60+CezIhaOZYviCbCRqXCDB7cVCWvUqlD2AYnxPVRE= v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= v0.2.1 h1:+EJdgffbfwIkBvnyx97mfS3slfn2o4UN/LWGm+mWVrQ= v0.2.1/go.mod h1:Z2ho3wmP4oCGON+c/RF+FJVsMb9zYZVsupp0c1a+SlQ= v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
......@@ -17,17 +57,77 @@ v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= v0.12.8 h1:4mEUzWb1HzRnxPwUevBX8g8ntsQ4rWw2R8CRB2QdZVI= v0.12.8/go.mod h1:C7e5A6bnWZT+nXkUwkvysGW7sxl/IGd63HEa6N/JY8s= v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE= v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
......@@ -2,7 +2,7 @@ package server
import (
......@@ -22,26 +22,42 @@ type displayState struct {
ShowChat bool `json:"show_chat"`
// state is the current state of the display.
// It is initialized with default values.
var state = displayState{
// Display struct defines the Display itself
type Display struct {
State displayState
clientsMu sync.Mutex
// clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients.
// Must acquire clientsMu before accessing the map.
// Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever.
// This is basically a *set* in go.
clients map[*sse.Client]bool
// the prefix used for logging
loggingPrefix string
// NewDisplay creates a new Display with default values
func NewDisplay(namespace string) *Display {
var loggingPrefix string
if namespace == "default" {
loggingPrefix = "display"
} else {
loggingPrefix = fmt.Sprintf("display[%s]", namespace)
return &Display{
State: displayState{
LargeChannel: "",
SmallChannel: "",
Volume: 0.5,
SmallScale: 0.3,
ShowChat: false,
clients: make(map[*sse.Client]bool),
loggingPrefix: loggingPrefix,
// clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients.
// Must acquire clientsMu before accessing the map.
// Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever.
// This is basically a *set* in go.
var (
clientsMu sync.Mutex
clients map[*sse.Client]bool = make(map[*sse.Client]bool)
// displayStateHandler updated the state based on the query string.
// stateHandler updated the state based on the query string.
// For example
// /display?large_channel=asdf&small_channel=null&volume=&small_scale=0.25&show_chat=true
// will
......@@ -52,9 +68,9 @@ var (
// - set show_chat to true
// If the floats (volume and small_scale) or the boolean (show_chat) could not be parsed, no changes are made at all and an error gets returned even if other fields could be parsed.
func displayStateHandler(w http.ResponseWriter, r *http.Request) {
func (d *Display) stateHandler(w http.ResponseWriter, r *http.Request) {
// the new state we will replace the current state with IF everything parses correctly.
newState := state
newState := d.State
// we save the error messages of the 5 parsing steps here.
var errs error
......@@ -117,18 +133,18 @@ func displayStateHandler(w http.ResponseWriter, r *http.Request) {
if errs != nil { // we had errors
http.Error(w, errs.Error(), http.StatusBadRequest)
log.Printf("Error(s) while parsing /display query: %v\n", errs)
logger.Printf("%s: Error(s) while parsing /display query: %v\n", d.loggingPrefix, errs)
if state != newState {
state = newState
if d.State != newState {
d.State = newState
body, err := json.Marshal(state)
body, err := json.Marshal(d.State)
if err != nil {
log.Printf("Updated display (%s):\n%s\n", r.URL.RawQuery, string(body))
logger.Printf("%s: Updated (%s):\n%s\n", d.loggingPrefix, r.URL.RawQuery, string(body))
// craft the SSE message
msg := sse.Msg{
......@@ -142,11 +158,11 @@ func displayStateHandler(w http.ResponseWriter, r *http.Request) {
// of clients to send to (while holding clientsMu) and start
// sending only after giving up the lock again.
var sendingTo []*sse.Client
for client := range clients {<