Skip to content

Instantly share code, notes, and snippets.

@twitchyliquid64
Last active June 24, 2017 21:21
Show Gist options
  • Save twitchyliquid64/05dcbcd47d8fbf82de75e097811e8359 to your computer and use it in GitHub Desktop.
Save twitchyliquid64/05dcbcd47d8fbf82de75e097811e8359 to your computer and use it in GitHub Desktop.
Pure-go binary to open a webcam, and expose it for watching on port 8080. Openable in VLC or a web browser.
package main
//Exposes a MJPEG stream of the connected camera in pure-Go.
//Linux/Raspberry pi only.
//Author: twitchyliquid64
//
//This script shows how to:
//1. Use blackjack's awesome V4L2 library to open a connected camera,
//2. Setup the format / frame size of the connected camera
//3. Optionally transcode YUYV format to JPEG
//4. Expose the resulting frames to a browser as an MJPEG stream.
//
// Credit:
// - github.com/c0va23 - mjpeg streaming code (HTTP handler)
// - github.com/blackjack - Webcam library and examples
// - github.com/AscendTech4H/AscendTechROV - YUYV decoding
import (
"bytes"
"errors"
"fmt"
"image"
"image/jpeg"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"reflect"
"time"
"github.com/blackjack/webcam"
)
var errUnsupportedCam = errors.New("could not select appropriate mode/framesize")
var source chan []byte //connection between frame recieve routine and frame server pump
var transcodeToUse transcoder
//convienent container for the required settings for a particular frame format.
//TranscoderConstructor returns a object that implements the transcoder interface
//transcoding allows us to consume formats which are not JPEG but still present
//a MJPEG stream.
type settingSpec struct {
Width uint32
Height uint32
TranscoderConstructor func(width, height uint32) transcoder
}
var allowedSettings = map[string]settingSpec{
"Motion-JPEG": settingSpec{
Width: 640,
Height: 480,
},
"YUYV 4:2:2": settingSpec{
Width: 640,
Height: 480,
TranscoderConstructor: func(width, height uint32) transcoder {
return &YUVToJPEGEncoder{
Width: int(width),
Height: int(height),
QualitySetting: 90,
}
},
},
}
type transcoder interface {
Encode([]byte) ([]byte, error)
}
// YUVToJPEGEncoder is an experimental YUV to JPEG encoder. For now we Only support the 4:2:2 ratio
// MAD PROPS to https://github.com/AscendTech4H/AscendTechROV who figured out all of this.
// Ive made minor modifications to support arbitrary width/heights.
type YUVToJPEGEncoder struct {
QualitySetting int
Width int
Height int
}
// Encode implements the transcoder interface.
func (e *YUVToJPEGEncoder) Encode(dat []byte) ([]byte, error) {
img := image.NewYCbCr(image.Rect(0, 0, e.Width, e.Height), image.YCbCrSubsampleRatio422)
pxgroups := make([]struct {
Y1, Cb, Y2, Cr uint8
}, (e.Width*e.Height)/2)
for i := range pxgroups {
v := &pxgroups[i]
addr := i * 4
slc := dat[addr : addr+4]
v.Y1 = slc[0]
v.Cb = slc[1]
v.Y2 = slc[2]
v.Cr = slc[3]
}
for i, v := range pxgroups {
x := (i * 2) % e.Width
x1, x2 := x, x+1
y := (i * 2) / e.Width
cpos := img.COffset(x, y)
ypos1 := img.YOffset(x1, y)
ypos2 := img.YOffset(x2, y)
img.Cb[cpos] = v.Cb
img.Cr[cpos] = v.Cr
img.Y[ypos1] = v.Y1
img.Y[ypos2] = v.Y2
}
output := new(bytes.Buffer)
output.Grow((e.Width * e.Height) / 2) //at least, probably, actually idk
err := jpeg.Encode(output, img, &jpeg.Options{Quality: e.QualitySetting})
return output.Bytes(), err
}
// Sets the frame format and size.
// Setting/frame-size selection is dictated by the values of the 'settings' global struct.
// The global 'transcodeToUse' is set if the particular settings spec requires it.
// This function returns the format+framesize settings selected, along with any error.
func setupFormat(cam *webcam.Webcam) (string, settingSpec, error) {
formats := cam.GetSupportedFormats()
for formatType, formatName := range formats {
if settings, ok := allowedSettings[formatName]; ok {
frameSizes := cam.GetSupportedFrameSizes(formatType)
for _, frameSize := range frameSizes {
fmt.Fprintf(os.Stderr, "[%s] Options candidate: %dx%d - %dx%d. Step=%d,%d\n", formatName, frameSize.MinWidth, frameSize.MinHeight, frameSize.MaxWidth, frameSize.MaxHeight, frameSize.StepWidth, frameSize.StepHeight)
}
for _, frameSize := range frameSizes {
if frameSize.MaxHeight <= settings.Height && frameSize.MaxWidth <= settings.Width && frameSize.MinHeight >= settings.Height && frameSize.MinWidth >= settings.Width {
f, w, h, err := cam.SetImageFormat(formatType, uint32(settings.Width), uint32(settings.Height))
if err != nil {
return "", settingSpec{}, err
}
fmt.Fprintf(os.Stderr, "Selected camera setting: %s (%dx%d)\n", formats[f], w, h)
if settings.TranscoderConstructor != nil {
transcodeToUse = settings.TranscoderConstructor(settings.Width, settings.Height)
fmt.Fprintf(os.Stderr, "Frames will be transcoded by %s\n", reflect.TypeOf(transcodeToUse))
} else {
fmt.Fprintln(os.Stderr, "No transcoder specified, frame data being passed through to clients unmodified")
}
return formatName, settings, nil
}
}
} else {
fmt.Fprintf(os.Stderr, "Skipping consideration of unsupported format type: %s\n", formatName)
}
}
return "", settingSpec{}, errUnsupportedCam
}
// Opens cam, sets up the format, and starts streaming.
func startCam() (*webcam.Webcam, error) {
cam, err := webcam.Open("/dev/video0") // Open webcam
if err != nil {
return nil, err
}
_, _, err = setupFormat(cam)
if err != nil {
return nil, err
}
err = cam.StartStreaming()
if err != nil {
return nil, err
}
return cam, nil
}
// HTTP handler to serve a MJPEG stream. Frames are obtained by a blocking read
// on the source channel.
// Caveats: If two clients connect to this stream, they will each get one half of the frames.
func mjpeg(responseWriter http.ResponseWriter, request *http.Request) {
fmt.Fprintf(os.Stderr, "mjpeg(): Start request %s\n", request.URL)
mimeWriter := multipart.NewWriter(responseWriter)
fmt.Fprintf(os.Stderr, "mjpeg(): Boundary: %s\n", mimeWriter.Boundary())
contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary())
responseWriter.Header().Add("Content-Type", contentType)
for {
frameStartTime := time.Now()
partHeader := make(textproto.MIMEHeader)
partHeader.Add("Content-Type", "image/jpeg")
partWriter, partErr := mimeWriter.CreatePart(partHeader)
if nil != partErr {
fmt.Fprintln(os.Stderr, partErr.Error())
break
}
snapshot := <-source
if _, writeErr := partWriter.Write(snapshot); nil != writeErr {
fmt.Fprintln(os.Stderr, writeErr.Error())
}
frameEndTime := time.Now()
frameDuration := frameEndTime.Sub(frameStartTime)
fps := float64(time.Second) / float64(frameDuration)
fmt.Fprintf(os.Stderr, "mjpeg(): Frame time: %s (%.2f)\n", frameDuration, fps)
}
}
// Convienence function to log a fatal message and exit.
func die(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}
// Start the listener waiting for
func runServer() {
listenAddr := ":8080"
serveMux := http.NewServeMux()
serveMux.HandleFunc("/", mjpeg)
fmt.Fprintf(os.Stderr, "Start listen on %s\n", listenAddr)
if err := http.ListenAndServe(listenAddr, serveMux); nil != err {
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
}
}
func main() {
source = make(chan []byte, 1)
cam, err := startCam()
if err != nil {
die("Cannot init camera: " + err.Error())
}
defer cam.Close()
go runServer()
for {
err = cam.WaitForFrame(1) //1 = 1s timeout
switch err.(type) {
case nil:
case *webcam.Timeout:
fmt.Fprint(os.Stderr, err.Error())
continue
default:
die("FATAL WaitForFrame(): " + err.Error())
}
frame, err := cam.ReadFrame()
//Transcoding allows us to transform wierd frame formats into JPEG
//as is required by a MJPEG stream.
if transcodeToUse != nil {
start := time.Now()
frame, err = transcodeToUse.Encode(frame)
if err != nil {
die("FATAL trancode failed: " + err.Error())
} else {
fmt.Fprintf(os.Stderr, "Transcode with %s took %v\n", reflect.TypeOf(transcodeToUse), time.Now().Sub(start))
}
}
if len(frame) != 0 {
select { //If passing on the frame would block, we drop the frame
case source <- frame:
default:
fmt.Fprintln(os.Stderr, "WARN: Dropping frame")
}
} else if err != nil {
die("FATAL ReadFrame(): " + err.Error())
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment