hz.tools/mjpeg: M-JPEG images in Go

Paul Tagliamonte 2021-02-28 project

Streaming video over the network is pretty annoying. There’s an endless ecosystem of encodings, protocols, and tactics that will deliver 4K video across the globe without hitting a single hiccip. M-JPEG, on the other hand, is designed to be extremely simple to create and display.

Code needed to serve an M-JPEG feed can be found at github.com/hztools/go-mjpeg, and imported by Go source as hz.tools/mjpeg.

M-JPEG Protocol

M-JPEG encodes a video by sending each frame as a JPEG image via an HTTP connection, using a MIME Multipart stream. If this is a client pulling from an M-JPEG stream coming from an HTTP server, you’ll get a HTTP response of Content-Type of multipart/x-mixed-replace, and then a set of chunks, each with a Content-Type of image/jpeg.

JPEG Image
JPEG Image

This means that the server only has to send a stream of JPEG images, which is very easy for someone without domain expertise in video codecs. From the client’s perspective, this means that displaying the video feed only requires storing the last “frame”, as bytes, and sending it as a JPEG. Additionally, including this in HTML is as easy as adding an HTML img tag.

 <img src="/webm-endpoint.mjpeg" alt="description" />

Example Server

Here, we can simulate some serious nostalgia (for those of us who had grown up fighting their over-the-air TV) by sending out a video stream that looks like the color of television, tuned to a dead channel.

package main

import (
	"image"
	"math/rand"
	"net/http"
	"time"

	"hz.tools/mjpeg"
)

func main() {
	i := image.NewGray(image.Rectangle{
		Min: image.Point{X: 0, Y: 0},
		Max: image.Point{X: 500, Y: 500},
	})

	stream := mjpeg.NewStream()
	go func() {
		for {

			for j := range i.Pix {
				i.Pix[j] = uint8(rand.Uint32())
			}

			time.Sleep(time.Second / 30)
			stream.Update(i)
		}
	}()

	http.Handle("/", stream)
	http.ListenAndServe("0.0.0.0:8888", nil)
}