diff --git a/cam2ip.go b/cam2ip.go index fc3789c..0c15a39 100644 --- a/cam2ip.go +++ b/cam2ip.go @@ -7,6 +7,7 @@ import ( "github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/server" + "github.com/gen2brain/cam2ip/video" ) const ( @@ -19,11 +20,12 @@ func main() { flag.IntVar(&srv.Index, "index", 0, "Camera index") flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds") - flag.Float64Var(&srv.FrameWidth, "frame-width", 640, "Frame width") - flag.Float64Var(&srv.FrameHeight, "frame-height", 480, "Frame height") + flag.Float64Var(&srv.FrameWidth, "frame-width", 640, "Camera frame width") + flag.Float64Var(&srv.FrameHeight, "frame-height", 480, "Camera frame height") flag.BoolVar(&srv.NoWebGL, "nowebgl", false, "Disable WebGL drawing of images (html handler)") flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address") flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled") + flag.StringVar(&srv.FileName, "video-file", "", "Use video file instead of camera") flag.Parse() srv.Name = name @@ -38,16 +40,35 @@ func main() { } } - srv.Camera, err = camera.New(srv.Index) - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - os.Exit(1) + if srv.FileName != "" { + if _, err = os.Stat(srv.FileName); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } } - srv.Camera.SetProperty(camera.PropFrameWidth, srv.FrameWidth) - srv.Camera.SetProperty(camera.PropFrameHeight, srv.FrameHeight) + if srv.FileName != "" { + vid, err := video.New(srv.FileName) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } - defer srv.Camera.Close() + srv.Reader = vid + } else { + cam, err := camera.New(srv.Index) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + + cam.SetProperty(camera.PropFrameWidth, srv.FrameWidth) + cam.SetProperty(camera.PropFrameHeight, srv.FrameHeight) + + srv.Reader = cam + } + + defer srv.Reader.Close() fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind) diff --git a/camera/camera.go b/camera/camera.go index 4bf556f..3ae335c 100644 --- a/camera/camera.go +++ b/camera/camera.go @@ -53,14 +53,12 @@ const ( // Camera represents camera. type Camera struct { - Index int camera *opencv.Capture } // New returns new Camera for given camera index. func New(index int) (camera *Camera, err error) { camera = &Camera{} - camera.Index = index camera.camera = opencv.NewCameraCapture(index) if camera.camera == nil { diff --git a/encoder/encode.go b/encoder/encode.go index 6164c95..96b10bb 100644 --- a/encoder/encode.go +++ b/encoder/encode.go @@ -1,3 +1,4 @@ +// Package encoder. package encoder import ( diff --git a/handlers/jpeg.go b/handlers/jpeg.go index a7baaa3..713aa24 100644 --- a/handlers/jpeg.go +++ b/handlers/jpeg.go @@ -4,18 +4,18 @@ import ( "log" "net/http" - "github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/reader" ) // JPEG handler. type JPEG struct { - camera *camera.Camera + reader reader.ImageReader } // NewJPEG returns new JPEG handler. -func NewJPEG(camera *camera.Camera) *JPEG { - return &JPEG{camera} +func NewJPEG(reader reader.ImageReader) *JPEG { + return &JPEG{reader} } // ServeHTTP handles requests on incoming connections. @@ -29,7 +29,7 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Add("Cache-Control", "no-store, no-cache") w.Header().Add("Content-Type", "image/jpeg") - img, err := j.camera.Read() + img, err := j.reader.Read() if err != nil { log.Printf("jpeg: read: %v", err) return diff --git a/handlers/mjpeg.go b/handlers/mjpeg.go index 83c0567..559d36e 100644 --- a/handlers/mjpeg.go +++ b/handlers/mjpeg.go @@ -9,19 +9,19 @@ import ( "net/textproto" "time" - "github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/reader" ) // MJPEG handler. type MJPEG struct { - camera *camera.Camera + reader reader.ImageReader delay int } // NewMJPEG returns new MJPEG handler. -func NewMJPEG(camera *camera.Camera, delay int) *MJPEG { - return &MJPEG{camera, delay} +func NewMJPEG(reader reader.ImageReader, delay int) *MJPEG { + return &MJPEG{reader, delay} } // ServeHTTP handles requests on incoming connections. @@ -56,7 +56,7 @@ loop: continue } - img, err := m.camera.Read() + img, err := m.reader.Read() if err != nil { log.Printf("mjpeg: read: %v", err) continue diff --git a/handlers/socket.go b/handlers/socket.go index 374a08a..0ced35f 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -8,29 +8,29 @@ import ( "golang.org/x/net/websocket" - "github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/reader" ) // Socket handler. type Socket struct { - camera *camera.Camera + reader reader.ImageReader delay int } // NewSocket returns new socket handler. -func NewSocket(camera *camera.Camera, delay int) websocket.Handler { - s := &Socket{camera, delay} +func NewSocket(reader reader.ImageReader, delay int) websocket.Handler { + s := &Socket{reader, delay} return websocket.Handler(s.write) } // write writes images to socket func (s *Socket) write(ws *websocket.Conn) { for { - img, err := s.camera.Read() + img, err := s.reader.Read() if err != nil { log.Printf("socket: read: %v", err) - continue + break } w := new(bytes.Buffer) diff --git a/reader/reader.go b/reader/reader.go new file mode 100644 index 0000000..7ac8999 --- /dev/null +++ b/reader/reader.go @@ -0,0 +1,15 @@ +// Package reader. +package reader + +import ( + "image" +) + +// ImageReader interface +type ImageReader interface { + // Read reads next frame from video and returns image. + Read() (img image.Image, err error) + + // Close closes camera/video. + Close() error +} diff --git a/server/server.go b/server/server.go index b573167..aa2721e 100644 --- a/server/server.go +++ b/server/server.go @@ -8,8 +8,8 @@ import ( "github.com/abbot/go-http-auth" - "github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/handlers" + "github.com/gen2brain/cam2ip/reader" ) // Server struct. @@ -20,13 +20,17 @@ type Server struct { Bind string Htpasswd string - Index int - Delay int + Index int + Delay int + FrameWidth float64 FrameHeight float64 - NoWebGL bool - Camera *camera.Camera + NoWebGL bool + + FileName string + + Reader reader.ImageReader } // NewServer returns new Server. @@ -44,10 +48,10 @@ func (s *Server) ListenAndServe() error { } http.Handle("/html", newAuthHandler(handlers.NewHTML(s.Bind, s.FrameWidth, s.FrameHeight, s.NoWebGL), basic)) - http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Camera), basic)) - http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Camera, s.Delay), basic)) + http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader), basic)) + http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay), basic)) - http.Handle("/socket", handlers.NewSocket(s.Camera, s.Delay)) + http.Handle("/socket", handlers.NewSocket(s.Reader, s.Delay)) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/video/video.go b/video/video.go new file mode 100644 index 0000000..878fbb9 --- /dev/null +++ b/video/video.go @@ -0,0 +1,50 @@ +// Package video. +package video + +import ( + "fmt" + "image" + + "github.com/lazywei/go-opencv/opencv" +) + +// Video represents video. +type Video struct { + video *opencv.Capture +} + +// New returns new Video for given path. +func New(filename string) (video *Video, err error) { + video = &Video{} + + video.video = opencv.NewFileCapture(filename) + if video.video == nil { + err = fmt.Errorf("video: can not open video %s", filename) + } + + return +} + +// Read reads next frame from video and returns image. +func (v *Video) Read() (img image.Image, err error) { + if v.video.GrabFrame() { + frame := v.video.RetrieveFrame(1) + img = frame.ToImage() + } else { + err = fmt.Errorf("video: can not grab frame") + } + + return +} + +// Close closes video. +func (v *Video) Close() (err error) { + if v.video == nil { + err = fmt.Errorf("video: video is not opened") + return + } + + v.video.Release() + v.video = nil + return +} diff --git a/video/video_test.go b/video/video_test.go new file mode 100644 index 0000000..865a865 --- /dev/null +++ b/video/video_test.go @@ -0,0 +1,62 @@ +package video + +import ( + "fmt" + "image/jpeg" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" +) + +func TestVideo(t *testing.T) { + video, err := New("ranko.mp4") + if err != nil { + t.Fatal(err) + } + + defer video.Close() + + tmpdir, err := ioutil.TempDir(os.TempDir(), "cam2ip") + if err != nil { + t.Error(err) + } + + defer os.RemoveAll(tmpdir) + + var i int + var n int = 10 + + timeout := time.After(time.Duration(n) * time.Second) + + for { + select { + case <-timeout: + //fmt.Printf("Fps: %d\n", i/n) + return + default: + i += 1 + + img, err := video.Read() + if err != nil { + t.Error(err) + } + + file, err := os.Create(filepath.Join(tmpdir, fmt.Sprintf("%03d.jpg", i))) + if err != nil { + t.Error(err) + } + + err = jpeg.Encode(file, img, &jpeg.Options{Quality: 75}) + if err != nil { + t.Error(err) + } + + err = file.Close() + if err != nil { + t.Error(err) + } + } + } +}