Record video using Golang and your webcam (without OpenCV/CGO/MingGW) — Part 2
A while ago I created a simple tool to provide a mostly hassle free and cross platform way to interact with my webcam. It consisted of front end written in Electron that would make use of the browser’s built in MediaDevices API to capture the webcam stream and a local Go server that would receive this stream in order to do any number of interesting things with it. It worked without downloading and compiling a bunch of C++ libraries which, to a self-taught and often bumbling developer like me, was a big plus.
I had been itching to use it for something other than saving ‘security’ footage of the crows lurking on my balcony and a few weeks ago I finally got the chance. I was tasked with adding a feature to a project that involved periodically saving an image from an MJPEG stream sent over TCP. Unfortunately, I did not have access to the actual camera (or an equivalent) so there was no way to test the feature without actually installing it on the customer site. I was looking forward to a nail biting few weeks of cobbling together some hopefully working code and crossing my fingers that said code actually worked. Needless to say bug fixing would be time consuming and awkward.
Fortunately Camtron came to the rescue. It only took a couple of hours to add a component that would forward my stream to the TCP listener in MJPEG format and allow me to see the new feature in action before I released it into the world. I not only got to make use of something I had written mostly for fun but I learned that a couple of assumptions I had made about the new feature I was developing were totally wrong saving me from an ego destroying moment of shame.
Using Camtron to transform and forward a video stream
To speed up things up I used the camtron-demo project that I threw together to test my video streaming library.
I added a ‘consumers’ folder to the root project directory and inside that create a file called forwardStream.go.
├── main.go
├── consumers
│ └── forwardstream.go
Then I added the package declaration and two functions, one to save the incoming stream to a temporary file and one to read that file and forward the stream to another server over TCP. The SaveVideo function takes a channel as its only argument. It is onto this channel that the camtron library pushes stream data as it comes in from the webcam handling front end.
package consumersfunc SaveVideo(streamChan chan []byte) {}func ForwardStream() {}
The SaveVideo function receives the video stream bytes from streamChan as they come in and saves them to a uniquely named temporary file. It pushes the name of this file to another channel so that the forwarding part of the process can be notified that it is available. (On a side note, Go channels are awesome! They make communicating between processes almost completely painless.)
var vidFilesToForwardChan chan string = make(chan string, 10)
var tempVidDir string = "videos"func SaveVideo(streamChan chan []byte) {
if _, err := os.Stat(tempVidDir); err != nil {
if os.IsNotExist(err) {
os.Mkdir(tempVidDir, os.ModePerm)
}
} var vidCount int = 1 var data []byte
for { select {
//receive bytes pushed onto channel by camtron library
case packet, ok := <-streamChan: if !ok {
log.Print("WARNING: Failed to get packet")
}
data = append(data, packet...) // save the video to a file every once in a while
if len(data) > 1000000 {
var index string = strconv.Itoa(vidCount)
tempFileName := "tempvid-" + index + ".webm"
// stop recording so that we get a new video
// after saving this one
camtron.StopRecording()
if !saveTempVid(tempFileName, data) {
return
} // push file name onto a channel to
// notify the forwarding process that
// it is available
vidFilesToForwardChan <- tempFileName // clear everything
data = nil
vidCount = vidCount + 1 // start recording to get a new video
camtron.StartRecording()
}
case val, _ := <-camtron.Context:
if val == "stop" {
close(streamChan)
log.Println("INFO: Shutting streaming to clients")
return
}
}
}
}//Standard function to save a file
func saveTempVid(fname string, video []byte) bool {
tempVidFile := tempVidDir + "/" + fname
vidFile, fileOpenErr := os.OpenFile(tempVidFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if fileOpenErr != nil {
log.Println(fileOpenErr)
}
defer vidFile.Close()
_, statErr := vidFile.Stat() if statErr != nil {
log.Println(statErr)
} _, writeErr := vidFile.Write(video)
if writeErr != nil {
log.Println(writeErr)
return false
} return true
}
The ForwardStream function runs FFmpeg using exec.Command. It receives the name of the temporary video file as it becomes available then hands it off to FFmpeg which reads the contents and sends the video to one or more urls over TCP as an MJPEG stream.
var targetUrls = []string{"tct://192.168.1.10:5000", "tcp://192.168.1.11:5000"}func ForwardStream() {
for {
select {
case vidFIleToForward, ok := <-vidFilesToForwardChan:
if !ok {
log.Print("WARNING: Something went wrong getting file to forward")
} sourveVidFile := tempVidDir + "/" + vidFIleToForward
for _, targetUrl := range targetUrls {
runFFMPEGToTCP(sourveVidFile, targetUrl)
} //clean up the temp file
err := os.Remove(sourveVidFile)
if err != nil {
log.Println(err)
}
}
}
}func runFFMPEGToTCP(sourceFile string, targetURL string) {
cmd := exec.Command("ffmpeg", "-i", sourceFile, "-f", "mjpeg", targetURL) err := cmd.Start()
if err != nil {
log.Panic(err)
} err = cmd.Wait()
if err != nil {
log.Println(err)
}
}
Finally, I just needed to create a function to register the video stream channel with the Camtron library and start up SaveVideo and ForwardStream as separate processes.
func StartForwardStreamConsumer() {
vidStream := make(chan []byte, 10)
camtron.RegisterStream(vidStream)
go SaveVideo(vidStream)
go ForwardStream()
}
Now I had a component plugged in the the Camtron streaming process (including the imports that VSCode magically adds for me).
package consumersimport (
"log"
"os"
"os/exec"
"strconv""github.com/vee2xx/camtron"
)var vidFilesToForwardChan chan string = make(chan string, 10)
var tempVidDir string = "videos"func SaveVideo(streamChan chan []byte) {
if _, err := os.Stat(tempVidDir); err != nil {
if os.IsNotExist(err) {
os.Mkdir(tempVidDir, os.ModePerm)
}
}
var vidCount int = 1
var data []byte
for {
select {
//receive bytes pushed onto channel by camtron library
case packet, ok := <-streamChan:
if !ok {
log.Print("WARNING: Failed to get packet")
} data = append(data, packet...)
// save the video to a file every once in a while
if len(data) > 1000000 {
var index string = strconv.Itoa(vidCount)
tempFileName := "tempvid-" + index + ".webm" // stop recording so that we get a new video
// after saving this one
camtron.StopRecording()
if !saveTempVid(tempFileName, data) {
return
}
// push file name onto a channel to
// notify the forwarding process that
// it is available
vidFilesToForwardChan <- tempFileName
// clear everything
data = nil
vidCount = vidCount + 1
// start recording to get a new video
camtron.StartRecording()
}
case val, _ := <-camtron.Context:
if val == "stop" {
close(streamChan)
log.Println("INFO: Shutting streaming to clients")
return
}
}
}
}//Standard function to save a file
func saveTempVid(fname string, video []byte) bool {
tempVidFile := tempVidDir + "/" + fname
vidFile, fileOpenErr := os.OpenFile(tempVidFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if fileOpenErr != nil {
log.Println(fileOpenErr)
}
defer vidFile.Close()
_, statErr := vidFile.Stat()
if statErr != nil {
log.Println(statErr)
}
_, writeErr := vidFile.Write(video)
if writeErr != nil {
log.Println(writeErr)
return false
}
return true
}var targetUrls = []string{"tcp://192.168.1.10:5000", "tcp://192.168.1.11:5000"}func ForwardStream() {
for {
select {
case vidFIleToForward, ok := <-vidFilesToForwardChan:
if !ok {
log.Print("WARNING: Something went wrong getting file to forward")
}
sourveVidFile := tempVidDir + "/" + vidFIleToForward
for _, targetUrl := range targetUrls {
runFFMPEGToTCP(sourveVidFile, targetUrl)
}
//clean up the temp file
err := os.Remove(sourveVidFile)
if err != nil {
log.Println(err)
}
}
}
}
func runFFMPEGToTCP(sourceFile string, targetURL string) {
cmd := exec.Command("ffmpeg", "-i", sourceFile, "-f", "mjpeg", targetURL)
err := cmd.Start()
if err != nil {
log.Panic(err)
}
err = cmd.Wait()
if err != nil {
log.Println(err)
}
}func StartForwardStreamConsumer() {
vidStream := make(chan []byte, 10)
camtron.RegisterStream(vidStream)
go SaveVideo(vidStream)
go ForwardStream()
}
The last thing to do was call StartForwardStreamConsumer from the project’s main function.
package mainimport (
"camtron-demo/consumers" "github.com/vee2xx/camtron"
)func main() {
consumers.StartForwardStreamConsumer()
camtron.StartCam()
}
It’s all a bit workaround-y with the need to save temporary files and run FFmpeg essentially from the command line but it did the job and I was able to do some end to end testing which not only sped up development time but significantly lowered my stress levels.
I don’t take the projects I mess around with in my spare time seriously as I am no world disrupting genius so its nice to be reminded that value can come from all sorts of unexpected places and something does not have to be super sophisticated to be useful!