Last active
June 24, 2017 21:21
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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