diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfa9083 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +examples/getcontrols/getcontrols +examples/http_mjpeg_streamer/http_mjpeg_streamer +examples/stdout_streamer/stdout_streamer +examples/imageserver/imageserver diff --git a/examples/getcontrols/getcontrols.go b/examples/getcontrols/getcontrols.go index eefd062..e5d5324 100644 --- a/examples/getcontrols/getcontrols.go +++ b/examples/getcontrols/getcontrols.go @@ -4,17 +4,24 @@ package main import ( "flag" "fmt" + "sort" "github.com/blackjack/webcam" ) var device = flag.String("input", "/dev/video0", "Input video device") +type control struct { + id webcam.ControlID + name string + min, max int32 +} + func main() { flag.Parse() cam, err := webcam.Open(*device) if err != nil { - panic(err.Error()) + panic(fmt.Errorf("%s: %v", *device, err.Error())) } defer cam.Close() @@ -34,7 +41,20 @@ func main() { cmap := cam.GetControls() fmt.Println("Available controls: ") - for id, c := range cmap { - fmt.Printf("ID:%08x %-32s Min: %4d Max: %5d\n", id, c.Name, c.Min, c.Max) + var clist []control + for id, cm := range cmap { + var c control + c.id = id + c.name = cm.Name + c.min = cm.Min + c.max = cm.Max + clist = append(clist, c) + } + sort.Slice(clist, func(i, j int) bool { + return clist[i].name < clist[j].name + }) + for _, cl := range clist { + fmt.Printf("ID:%08x %-32s Min: %4d Max: %5d\n", cl.id, + cl.name, cl.min, cl.max) } } diff --git a/examples/http_mjpeg_streamer/webcam.go b/examples/http_mjpeg_streamer/webcam.go index adfa08c..54a031a 100644 --- a/examples/http_mjpeg_streamer/webcam.go +++ b/examples/http_mjpeg_streamer/webcam.go @@ -118,7 +118,7 @@ FMT: } fmt.Fprintln(os.Stderr, "Requesting", format_desc[format], size.GetString()) - f, w, h, err := cam.SetImageFormat(format, uint32(size.MaxWidth), uint32(size.MaxHeight)) + f, w, h, _, _, err := cam.SetImageFormat(format, uint32(size.MaxWidth), uint32(size.MaxHeight)) if err != nil { log.Println("SetImageFormat return error", err) return diff --git a/examples/imageserver/README.md b/examples/imageserver/README.md new file mode 100644 index 0000000..244db54 --- /dev/null +++ b/examples/imageserver/README.md @@ -0,0 +1,2 @@ +# imageserver +A simple web server to capture still images from a webcam. diff --git a/examples/imageserver/imageserver.service b/examples/imageserver/imageserver.service new file mode 100644 index 0000000..b4036a9 --- /dev/null +++ b/examples/imageserver/imageserver.service @@ -0,0 +1,16 @@ +[Unit] +Description=webcam image server +After=network.target + +[Service] +User=root +Type=simple +TimeoutStopSec=10 +ExecStart=/usr/local/bin/imageserver --delay=2 --controls=focus=175,power_line_frequency=1 + +Restart=on-failure +RestartSec=15s +SuccessExitStatus=SIGKILL + +[Install] +WantedBy=default.target diff --git a/examples/imageserver/server.go b/examples/imageserver/server.go new file mode 100644 index 0000000..fc9be9e --- /dev/null +++ b/examples/imageserver/server.go @@ -0,0 +1,148 @@ +// Program that serves images taken from a webcam. +package main + +import ( + "flag" + "fmt" + "image/gif" + "image/jpeg" + "image/png" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/blackjack/webcam" + "github.com/blackjack/webcam/frame" + "github.com/blackjack/webcam/snapshot" +) + +var port = flag.Int("port", 8080, "Web server port number") +var path = flag.String("path", "image", "Image base filename") +var device = flag.String("input", "/dev/video0", "Input video device") +var resolution = flag.String("resolution", "800x600", "Camera resolution") +var format = flag.String("format", "YUYV", "Pixel format of camera") +var controls = flag.String("controls", "", + "Control parameters for camera (use --controls=list to list controls)") +var startDelay = flag.Int("delay", 2, "Delay at start (seconds)") +var verbose = flag.Bool("v", false, "Log more information") + +var cnames map[string]webcam.ControlID = map[string]webcam.ControlID{ + "focus": 0x009a090a, + "power_line_frequency": 0x00980918, + "brightness": 0x00980900, + "contrast": 0x00980901, + "autoiso": 0x009a0918, + "autoexp": 0x009a0901, + "saturation": 0x00980902, + "sharpness": 0x0098091b, + "rotate": 0x00980922, + "stabilization": 0x009a0916, +} + +func main() { + flag.Parse() + s := strings.Split(*resolution, "x") + if len(s) != 2 { + log.Fatalf("%s: Illegal resolution", *resolution) + } + x, err := strconv.Atoi(s[0]) + if err != nil { + log.Fatalf("%s: illegal width: %v", s[0], err) + } + y, err := strconv.Atoi(s[1]) + if err != nil { + log.Fatalf("%s: illegal height: %v", s[1], err) + } + if *controls == "list" { + fmt.Printf("Control list (not all cameras may support all options):\n") + for c, _ := range cnames { + fmt.Printf(" %s\n", c) + } + return + } + if *startDelay != 0 { + time.Sleep(time.Duration(*startDelay) * time.Second) + } + cm := snapshot.NewSnapper() + if err := cm.Open(*device, frame.FourCC(*format), x, y); err != nil { + log.Fatalf("%s: %v", *device, err) + } + if *startDelay != 0 { + time.Sleep(time.Duration(*startDelay) * time.Second) + } + defer cm.Close() + // Set camera controls. + if len(*controls) != 0 { + for _, control := range strings.Split(*controls, ",") { + s := strings.Split(control, "=") + if len(s) != 2 { + log.Fatalf("Bad control option: %s", control) + } + id, ok := cnames[s[0]] + if !ok { + log.Fatalf("%s: Unknown control", s[0]) + } + val, err := strconv.Atoi(s[1]) + if err != nil { + log.Fatalf("Bad control value: %s (%v)", control, err) + } + if *verbose { + fmt.Printf("Setting control '%s' to %d\n", s[0], val) + } + if err = cm.SetControl(id, int32(val)); err != nil { + log.Fatalf("SetControl error: %s (%v)", control, err) + } + } + } + encodeJpeg := func(w http.ResponseWriter, f frame.Frame) error { + w.Header().Set("Content-Type", "image/jpeg") + return jpeg.Encode(w, f, nil) + } + encodePNG := func(w http.ResponseWriter, f frame.Frame) error { + w.Header().Set("Content-Type", "image/png") + return png.Encode(w, f) + } + encodeGIF := func(w http.ResponseWriter, f frame.Frame) error { + w.Header().Set("Content-Type", "image/gif") + return gif.Encode(w, f, nil) + } + http.Handle(fmt.Sprintf("/%s.jpg", *path), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + publishImage(cm, w, r, encodeJpeg) + })) + http.Handle(fmt.Sprintf("/%s.jpeg", *path), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + publishImage(cm, w, r, encodeJpeg) + })) + http.Handle(fmt.Sprintf("/%s.png", *path), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + publishImage(cm, w, r, encodePNG) + })) + http.Handle(fmt.Sprintf("/%s.gif", *path), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + publishImage(cm, w, r, encodeGIF) + })) + url := fmt.Sprintf(":%d", *port) + if *verbose { + log.Printf("Starting server on %s", url) + } + server := &http.Server{Addr: url} + log.Fatal(server.ListenAndServe()) +} + +func publishImage(cm *snapshot.Snapper, w http.ResponseWriter, r *http.Request, encode func(http.ResponseWriter, frame.Frame) error) { + if *verbose { + log.Printf("URL request: %v", r.URL) + } + f, err := cm.Snap() + if err != nil { + log.Printf("Getframe: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer f.Release() + if err := encode(w, f); err != nil { + log.Printf("Error writing image: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } else if *verbose { + log.Printf("Wrote image successfully\n") + } +} diff --git a/examples/stdout_streamer/stdout_streamer.go b/examples/stdout_streamer/stdout_streamer.go index af961c1..0d3391f 100644 --- a/examples/stdout_streamer/stdout_streamer.go +++ b/examples/stdout_streamer/stdout_streamer.go @@ -74,7 +74,7 @@ func main() { choice = readChoice(fmt.Sprintf("Choose format [1-%d]: ", len(frames))) size := frames[choice-1] - f, w, h, err := cam.SetImageFormat(format, uint32(size.MaxWidth), uint32(size.MaxHeight)) + f, w, h, _, _, err := cam.SetImageFormat(format, uint32(size.MaxWidth), uint32(size.MaxHeight)) if err != nil { panic(err.Error()) diff --git a/frame/frame.go b/frame/frame.go new file mode 100644 index 0000000..da73e5e --- /dev/null +++ b/frame/frame.go @@ -0,0 +1,53 @@ +// package frame wraps raw webcam frames as an image. +package frame + +import ( + "fmt" + "image" + + "github.com/blackjack/webcam" +) + +type FourCC string + +// Release is called when the frame is no longer in use. +// The implementation may set a finalizer on the frame as a precaution +// in case Release is not called (which would cause a kernel resource leak). +type Frame interface { + image.Image + Release() +} + +var framerFactoryMap = map[FourCC]func(int, int, int, int) func([]byte, func()) (Frame, error){} + +// RegisterFramer registers a framer factory for a format. +// Note that only one handler can be registered for any single format. +func RegisterFramer(format FourCC, factory func(int, int, int, int) func([]byte, func()) (Frame, error)) { + framerFactoryMap[format] = factory +} + +// GetFramer returns a function that wraps the frame for this format. +func GetFramer(format FourCC, w, h, stride, size int) (func([]byte, func()) (Frame, error), error) { + if factory, ok := framerFactoryMap[format]; ok { + return factory(w, h, stride, size), nil + } + return nil, fmt.Errorf("No handler for format '%s'", format) +} + +// PixelFormatToFourCC converts the v4l2 PixelFormat to a FourCC. +func PixelFormatToFourCC(pf webcam.PixelFormat) FourCC { + b := make([]byte, 4) + b[0] = byte(pf) + b[1] = byte(pf >> 8) + b[2] = byte(pf >> 16) + b[3] = byte(pf >> 24) + return FourCC(b) +} + +// FourCCToPixelFormat converts the four character string to a v4l2 PixelFormat. +func FourCCToPixelFormat(f FourCC) (webcam.PixelFormat, error) { + if len(f) != 4 { + return 0, fmt.Errorf("%s: Illegal FourCC", f) + } + return webcam.PixelFormat(uint32(f[0]) | uint32(f[1])<<8 | uint32(f[2])<<16 | uint32(f[3])<<24), nil +} diff --git a/frame/jpeg.go b/frame/jpeg.go new file mode 100644 index 0000000..138062a --- /dev/null +++ b/frame/jpeg.go @@ -0,0 +1,61 @@ +package frame + +import ( + "bytes" + "image" + "image/color" + "image/jpeg" + "runtime" +) + +type fJPEG struct { + img image.Image + release func() +} + +// Register this framer for this format. +func init() { + RegisterFramer("JPEG", newJPEGFramer) +} + +// Return a framer for JPEG. +func newJPEGFramer(w, h, stride, size int) func([]byte, func()) (Frame, error) { + return jpegFramer +} + +// Wrap a jpeg block in a Frame so that it can be used as an image. +func jpegFramer(f []byte, rel func()) (Frame, error) { + img, err := jpeg.Decode(bytes.NewBuffer(f)) + if err != nil { + if rel != nil { + rel() + } + return nil, err + } + fr := &fJPEG{img: img, release: rel} + runtime.SetFinalizer(fr, func(obj Frame) { + obj.Release() + }) + return fr, nil +} + +func (f *fJPEG) ColorModel() color.Model { + return f.img.ColorModel() +} + +func (f *fJPEG) Bounds() image.Rectangle { + return f.img.Bounds() +} + +func (f *fJPEG) At(x, y int) color.Color { + return f.img.At(x, y) +} + +// Done with frame, release back to camera (if required). +func (f *fJPEG) Release() { + if f.release != nil { + f.release() + // Make sure it only gets called once. + f.release = nil + } +} diff --git a/frame/mjpeg.go b/frame/mjpeg.go new file mode 100644 index 0000000..fc7275c --- /dev/null +++ b/frame/mjpeg.go @@ -0,0 +1,172 @@ +package frame + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/jpeg" + "runtime" +) + +type fMJPEG struct { + img image.Image + release func() +} + +const ( + sectionFlag = 0xFF + soiMarker = 0xd8 + eoiMarker = 0xd9 + dhtMarker = 0xc4 + sosMarker = 0xda +) + +// Default Huffman tables. +var default_dht []byte = []byte{ + 0xff, 0xc4, 0x01, 0xa2, + + 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + + 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + + 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, + 0x04, 0x00, 0x00, 0x01, 0x7d, + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, + 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, + 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, + 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, + 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, + 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, + + 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, + 0x04, 0x00, 0x01, 0x02, 0x77, + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, + 0x51, 0x07, 0x61, 0x71, + 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, + 0x23, 0x33, 0x52, 0xf0, + 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, + 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, + 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, + 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, + 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, + 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, +} + +// Register this framer for this format. +func init() { + RegisterFramer("MJPG", newMJPGFramer) +} + +func newMJPGFramer(w, h, stride, size int) func([]byte, func()) (Frame, error) { + return mjpegFramer +} + +// Wrap a mjpeg block in a Frame so that it can be used as an image. +// The standard jpeg decoding does not work if there are no Huffman tables, +// so check the frame and add a default table if required. +func mjpegFramer(f []byte, rel func()) (Frame, error) { + img, err := decodeMJPEG(f) + if err != nil { + if rel != nil { + rel() + } + return nil, err + } + fr := &fMJPEG{img: img, release: rel} + runtime.SetFinalizer(fr, func(obj Frame) { + obj.Release() + }) + return fr, nil +} + +// decodeMJPEG decodes the frame into an image. +func decodeMJPEG(f []byte) (image.Image, error) { + sect, err := findConfig(f) + if err != nil { + return nil, err + } + _, ok := sect[dhtMarker] + var buf *bytes.Buffer + if !ok { + s, ok := sect[sosMarker] + if !ok { + return nil, fmt.Errorf("no scan data in image") + } + // Insert the default Huffman table before the start + // of the scan data. + ins := s[0] + buf = new(bytes.Buffer) + buf.Write(f[:ins]) + buf.Write(default_dht) + buf.Write(f[ins:]) + } else { + buf = bytes.NewBuffer(f) + } + return jpeg.Decode(buf) +} + +func (f *fMJPEG) ColorModel() color.Model { + return f.img.ColorModel() +} + +func (f *fMJPEG) Bounds() image.Rectangle { + return f.img.Bounds() +} + +func (f *fMJPEG) At(x, y int) color.Color { + return f.img.At(x, y) +} + +// Done with frame, release back to camera (if required). +func (f *fMJPEG) Release() { + if f.release != nil { + f.release() + // Make sure it only gets called once. + f.release = nil + } +} + +// findConfig returns a map of the different config markers and their location. +func findConfig(f []byte) (map[byte][]int, error) { + m := make(map[byte][]int) + for l := 0; l < len(f)-1; { + if f[l] != sectionFlag { + return nil, fmt.Errorf("No section marker at location %d", l) + } + l++ + marker := f[l] + m[marker] = append(m[marker], l-1) + l++ + if marker == soiMarker || marker == eoiMarker { + continue + } + if marker == sosMarker { + break + } + // next 2 bytes are length of the section (big-endian). + if l >= len(f)-2 { + return nil, fmt.Errorf("unexpected EOF at location %d", l) + } + l += (int(f[l]) << 8) + int(f[l+1]) + } + return m, nil +} diff --git a/frame/rgb.go b/frame/rgb.go new file mode 100644 index 0000000..4142c96 --- /dev/null +++ b/frame/rgb.go @@ -0,0 +1,80 @@ +package frame + +import ( + "fmt" + "image" + "image/color" + "runtime" +) + +type fRGB struct { + model color.Model + b image.Rectangle + stride int + roffs int + goffs int + boffs int + frame []byte + release func() +} + +// Register framers for these formats. +func init() { + RegisterFramer("RGB3", newFramerRGB3) + RegisterFramer("BGR3", newFramerBGR3) +} + +// Return a function that is used as a framer for RGB3. +func newFramerRGB3(w, h, stride, size int) func([]byte, func()) (Frame, error) { + return newRGBFramer(w, h, stride, size, 0, 1, 2) +} + +// Return a function that is used as a framer for BGR3. +func newFramerBGR3(w, h, stride, size int) func([]byte, func()) (Frame, error) { + return newRGBFramer(w, h, stride, size, 2, 1, 0) +} + +// Return a function that is used as a generic RGB framer. +func newRGBFramer(w, h, stride, size, r, g, b int) func([]byte, func()) (Frame, error) { + return func(buf []byte, rel func()) (Frame, error) { + return frameRGB(size, stride, w, h, r, g, b, buf, rel) + } +} + +// Wrap a raw webcam frame in a Frame so that it can be used as an image. +func frameRGB(size, stride, w, h, rof, gof, bof int, b []byte, rel func()) (Frame, error) { + if len(b) != size { + if rel != nil { + defer rel() + } + return nil, fmt.Errorf("Wrong frame length (exp: %d, read %d)", size, len(b)) + } + f := &fRGB{model: color.RGBAModel, b: image.Rect(0, 0, w, h), stride: stride, + roffs: rof, goffs: gof, boffs: bof, frame: b, release: rel} + runtime.SetFinalizer(f, func(obj Frame) { + obj.Release() + }) + return f, nil +} + +func (f *fRGB) ColorModel() color.Model { + return f.model +} + +func (f *fRGB) Bounds() image.Rectangle { + return f.b +} + +func (f *fRGB) At(x, y int) color.Color { + i := f.stride*y + x*3 + return color.RGBA{f.frame[i+f.roffs], f.frame[i+f.goffs], f.frame[i+f.boffs], 0xFF} +} + +// Done with frame, release back to camera (if required). +func (f *fRGB) Release() { + if f.release != nil { + f.release() + // Make sure it only gets called once. + f.release = nil + } +} diff --git a/frame/yuyv422.go b/frame/yuyv422.go new file mode 100644 index 0000000..fecc967 --- /dev/null +++ b/frame/yuyv422.go @@ -0,0 +1,69 @@ +package frame + +import ( + "fmt" + "image" + "image/color" + "runtime" +) + +type fYUYV422 struct { + model color.Model + b image.Rectangle + stride int + size int + frame []byte + release func() +} + +// Register a framer factory for this format. +func init() { + RegisterFramer("YUYV", newFramerYUYV422) +} + +func newFramerYUYV422(w, h, stride, size int) func([]byte, func()) (Frame, error) { + return func(b []byte, rel func()) (Frame, error) { + return frameYUYV422(size, stride, w, h, b, rel) + } +} + +// Wrap a raw webcam frame in a Frame so that it can be used as an image. +func frameYUYV422(size, stride, w, h int, b []byte, rel func()) (Frame, error) { + if len(b) != size { + if rel != nil { + defer rel() + } + return nil, fmt.Errorf("Wrong frame length (exp: %d, read %d)", size, len(b)) + } + f := &fYUYV422{model: color.YCbCrModel, b: image.Rect(0, 0, w, h), stride: stride, frame: b, release: rel} + runtime.SetFinalizer(f, func(obj Frame) { + obj.Release() + }) + return f, nil +} + +func (f *fYUYV422) ColorModel() color.Model { + return f.model +} + +func (f *fYUYV422) Bounds() image.Rectangle { + return f.b +} + +func (f *fYUYV422) At(x, y int) color.Color { + index := f.stride*y + (x&^1)*2 + if x&1 == 0 { + return color.YCbCr{f.frame[index], f.frame[index+1], f.frame[index+3]} + } else { + return color.YCbCr{f.frame[index+2], f.frame[index+1], f.frame[index+3]} + } +} + +// Done with frame, release back to camera (if required). +func (f *fYUYV422) Release() { + if f.release != nil { + f.release() + // Make sure it only gets called once. + f.release = nil + } +} diff --git a/ioctl/ioctl.go b/ioctl/ioctl.go index 3ce8df6..a1118f3 100644 --- a/ioctl/ioctl.go +++ b/ioctl/ioctl.go @@ -21,6 +21,8 @@ const ( typeShift = numberShift + numberBits sizeShift = typeShift + typeBits directionShift = sizeShift + sizeBits + + ErrEINVAL = unix.EINVAL ) func ioc(dir, t, nr, size uintptr) uintptr { diff --git a/snapshot/snapshot.go b/snapshot/snapshot.go new file mode 100644 index 0000000..d87965d --- /dev/null +++ b/snapshot/snapshot.go @@ -0,0 +1,174 @@ +// snapshot extracts image frames from a video stream. +package snapshot + +import ( + "fmt" + + "github.com/blackjack/webcam" + "github.com/blackjack/webcam/frame" +) + +const ( + defaultTimeout = 5 + defaultBuffers = 16 +) + +type snap struct { + frm []byte + index uint32 +} + +type Snapper struct { + cam *webcam.Webcam + Timeout uint32 + Buffers uint32 + framer func([]byte, func()) (frame.Frame, error) + stop chan struct{} + stream chan snap +} + +// NewSnapper creates a new Snapper. +func NewSnapper() *Snapper { + return &Snapper{Timeout: defaultTimeout, Buffers: defaultBuffers} +} + +// Close releases all current frames and shuts down the webcam. +func (c *Snapper) Close() { + if c.cam != nil { + c.stop <- struct{}{} + // Flush any remaining frames. + for f := range c.stream { + c.cam.ReleaseFrame(f.index) + } + c.cam.StopStreaming() + c.cam.Close() + c.cam = nil + } +} + +// Open initialises the webcam ready for use, and begins streaming. +func (c *Snapper) Open(device string, format frame.FourCC, w, h int) (ret error) { + pf, err := frame.FourCCToPixelFormat(format) + if err != nil { + return err + } + if c.cam != nil { + c.Close() + } + cam, err := webcam.Open(device) + if err != nil { + return err + } + // Add a defer function that closes the camera in the event of an error. + defer func() { + if ret != nil { + close(c.stream) + c.Close() + } + }() + c.cam = cam + c.stop = make(chan struct{}, 1) + c.stream = make(chan snap, 0) + // Get the supported formats and their descriptions. + _, ok := c.cam.GetSupportedFormats()[pf] + if !ok { + return fmt.Errorf("%s: unsupported format: %s", device, format) + } + var found bool + for _, value := range c.cam.GetSupportedFrameSizes(pf) { + if Match(value, w, h) { + found = true + break + } + } + if !found { + return fmt.Errorf("%s: unsupported resolution: %dx%d", device, w, h) + } + npf, nw, nh, stride, size, err := c.cam.SetImageFormat(pf, uint32(w), uint32(h)) + + if err != nil { + return err + } + if npf != pf || w != int(nw) || h != int(nh) { + fmt.Printf("Asked for %08x %dx%d, got %08x %dx%d\n", pf, w, h, npf, nw, nh) + } + if c.framer, err = frame.GetFramer(format, w, h, int(stride), int(size)); err != nil { + return err + } + + c.cam.SetBufferCount(c.Buffers) + c.cam.SetAutoWhiteBalance(true) + if err := c.cam.StartStreaming(); err != nil { + return err + } + go c.capture() + return nil +} + +// Snap returns one frame from the camera. +func (c *Snapper) Snap() (frame.Frame, error) { + snap, ok := <-c.stream + if !ok { + return nil, fmt.Errorf("No frame received") + } + return c.framer(snap.frm, func() { + c.cam.ReleaseFrame(snap.index) + }) +} + +// capture continually reads frames and either discards the frames or +// sends them to a channel that is ready. +func (c *Snapper) capture() { + for { + err := c.cam.WaitForFrame(c.Timeout) + + switch err.(type) { + case nil: + case *webcam.Timeout: + continue + default: + panic(err) + } + + frame, index, err := c.cam.GetFrame() + if err != nil { + panic(err) + } + select { + // Only executed if stream is ready to receive. + case c.stream <- snap{frame, index}: + // Signal to stop streaming. + case <-c.stop: + // Finish up. + c.cam.ReleaseFrame(index) + close(c.stream) + return + default: + c.cam.ReleaseFrame(index) + } + } +} + +// GetControl returns the current value of a camera control. +func (c *Snapper) GetControl(id webcam.ControlID) (int32, error) { + return c.cam.GetControl(id) +} + +// SetControl sets the selected camera control. +func (c *Snapper) SetControl(id webcam.ControlID, value int32) error { + return c.cam.SetControl(id, value) +} + +// Return true if frame size can accomodate request. +func Match(fs webcam.FrameSize, w, h int) bool { + return canFit(fs.MinWidth, fs.MaxWidth, fs.StepWidth, uint32(w)) && + canFit(fs.MinHeight, fs.MaxHeight, fs.StepHeight, uint32(h)) +} + +func canFit(min, max, step, val uint32) bool { + // Fixed size exact match. + if min == max && step == 0 && val == min { + return true + } + return step != 0 && val >= min && val <= max && ((val-min)%step) == 0 +} diff --git a/v4l2.go b/v4l2.go index e573783..9b6e9db 100644 --- a/v4l2.go +++ b/v4l2.go @@ -288,7 +288,7 @@ func getFrameSize(fd uintptr, index uint32, code uint32) (frameSize FrameSize, e return } -func setImageFormat(fd uintptr, formatcode *uint32, width *uint32, height *uint32) (err error) { +func setImageFormat(fd uintptr, formatcode, width, height, stride, size *uint32) (err error) { format := &v4l2_format{ _type: V4L2_BUF_TYPE_VIDEO_CAPTURE, @@ -326,6 +326,8 @@ func setImageFormat(fd uintptr, formatcode *uint32, width *uint32, height *uint3 *width = pixReverse.Width *height = pixReverse.Height *formatcode = pixReverse.Pixelformat + *stride = pixReverse.Bytesperline + *size = pixReverse.Sizeimage return diff --git a/webcam.go b/webcam.go index 19d3559..76b7474 100644 --- a/webcam.go +++ b/webcam.go @@ -6,8 +6,6 @@ package webcam import ( "errors" "golang.org/x/sys/unix" - "reflect" - "unsafe" ) // Webcam object @@ -109,18 +107,20 @@ func (w *Webcam) GetSupportedFrameSizes(f PixelFormat) []FrameSize { // Note, that device driver can change that values. // Resulting values are returned by a function // alongside with an error if any -func (w *Webcam) SetImageFormat(f PixelFormat, width, height uint32) (PixelFormat, uint32, uint32, error) { +func (w *Webcam) SetImageFormat(f PixelFormat, width, height uint32) (PixelFormat, uint32, uint32, uint32, uint32, error) { code := uint32(f) cw := width ch := height + var stride uint32 + var size uint32 - err := setImageFormat(w.fd, &code, &width, &height) + err := setImageFormat(w.fd, &code, &width, &height, &stride, &size) if err != nil { - return 0, 0, 0, err + return 0, 0, 0, 0, 0, err } else { - return PixelFormat(code), cw, ch, nil + return PixelFormat(code), cw, ch, stride, size, nil } } @@ -280,11 +280,3 @@ func (w *Webcam) SetAutoWhiteBalance(val bool) error { } return setControl(w.fd, V4L2_CID_AUTO_WHITE_BALANCE, v) } - -func gobytes(p unsafe.Pointer, n int) []byte { - - h := reflect.SliceHeader{uintptr(p), n, n} - s := *(*[]byte)(unsafe.Pointer(&h)) - - return s -}