Internet Jukebox
One of the hackerspaces at which I am a member installed a Beaglebone Black on their door and put a speaker outside.
Now we can play sounds to the street, but to do so, you have to ssh into the Beaglebone. That’s not bad, but it’s a hassle. Okay, so can I make a REST endpoint that plays music?
(The answer is yes.)
Here’s the repo. Let’s go through it.
Structure⌗
There are two endpoints, /
and /play/
. /
returns a simple HTML form that lets the user upload a sound file. When the user clicks Submit
, a POST
request is made to /play/
. The sound file is extracted from the form data, and asynchronously processed.
The sound file process is thus: it is saved to a temporary file on disk, mplayer
is called on that file, and then when that process terminates, the sound file is removed.
Serving a static html page⌗
is easy.
controlfs := http.FileServer(http.Dir("control"))
http.Handle("/", controlfs)
There’s a directory called control
, and it contains index.html
. When you hit /
, you get index.html. Wildly easy.
Creating that REST endpoint⌗
http.HandleFunc("/play/", func(w http.ResponseWriter, req *http.Request) {
if req.Method == "POST" {
if req.ContentLength > 10485760 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("File size capped at 10mb"))
return
}
soundFile, headers, err := req.FormFile("soundFile")
if err != nil {
log.Printf("Error getting soundFile from Form. \n %s", err.Error())
w.WriteHeader(http.StatusServiceUnavailable)
return
}
log.Printf("Recieved %s", headers.Filename)
w.Write([]byte("All done!"))
go playASound(soundFile)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
//TODO(cagocs): maybe return 200 with the name of the sound playing?
}
})
Briefly, here’s what we’re doing:
- Setting up a url pattern,
/play
/. - Defining an anonymous function that runs when you hit
/play
/ - Checking the request method.
- if it’s
POST
- Check the content-length. If it’s greater than 10 MiB, return a status code 400.
- Get the
soundFile
out of the form. - Log the filename
- Return a status code 200
- Asynchronously play the file
- if it isn’t
- Return a status code 405
- I considered returning a string representation of all the files playing, but didn’t.
- if it’s
One quick point: Yes, it’s possible to spoof the content-length in a request. I didn’t check for that. If you decide to run this in any sort of mission critical sense, maybe watch out for that.
Playing a sound⌗
func playASound(file multipart.File) { soundFile, err0 := ioutil.TempFile("", “sound_") if err0 != nil { log.Printf(“Error initializing new file”) }
buffer, err1 := ioutil.ReadAll(file)
if err1 != nil {
log.Printf("Error reading mime multipart file")
}
err2 := ioutil.WriteFile(soundFile.Name(), buffer, os.ModeTemporary)
if err2 != nil {
log.Printf("Error writing file to disk")
}
cmd := exec.Command("mplayer", soundFile.Name())
err3 := cmd.Run()
if err3 != nil {
log.Printf("Error playing file %s", soundFile.Name())
}
soundFile.Close()
err4 := os.Remove(soundFile.Name())
if err4 != nil {
log.Println("Error deleting %s", soundFile.Name())
}
}
So now we have a sound file in memory. How do we get it to the speakers? I spent a long time screwing around trying to figure out a “pure Go” solution, gave up, and decided to cheat. The Beaglebone Black will probably ship with MPlayer; why not use that?
I skimmed through some code examples and came up with the solution above. playASound
is running asynchronously, so it can spend some time doing what it needs to do. It creates a new TempFile
, and writes the sound file there. It then creates a Command
that calls mplayer
and passes the name of the temporary file to mplayer
as an argument. Mplayer
plays the file, and when the mplayer
process completes, our goroutine closes and removes the temporary file.
Running⌗
I decided to not take my own advice and open this up to the general internet a few days ago. I used port forwarding on my router to forward :3030 on my external IP address to port :3030 on my laptop, and ran the program. I posted about it on IRC, forums, and made an Imgur post, and I got a few people participating.
I found it to be incredibly stable. No crashing, no issues running 4 to 6 instances of mplayer on top of one another. I was pleased with how the server handled unexpected EOFs and connection timeouts. Lastly, I discovered that letting people assault my ears with a barrage of mp3s is fun!