/* DESCRIPTION geovision.go provides an implementation of the AVDevice interface for the GeoVision IP camera. AUTHORS Saxon A. Nelson-Milton LICENSE Copyright (C) 2019 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License in gpl.txt. If not, see http://www.gnu.org/licenses. */ package revid import ( "errors" "fmt" "net" "time" "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/input/gvctrl" "bitbucket.org/ausocean/av/protocol/rtcp" "bitbucket.org/ausocean/av/protocol/rtp" "bitbucket.org/ausocean/av/protocol/rtsp" "bitbucket.org/ausocean/utils/logger" ) // Configuration defaults. const ( defaultGVCameraIP = "192.168.1.50" defaultGVCodec = codecutil.H264 defaultGVHeight = 720 defaultGVFrameRate = 25 defaultGVBitrate = 400 defaultGVVBRBitrate = 400 defaultGVMinFrames = 100 defaultGVVBRQuality = "standard" ) // Configuration field errors. var ( errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting") errGVBadCodec = errors.New("codec bad or unset, defaulting") errGVBadFrameRate = errors.New("frame rate bad or unset, defaulting") errGVBadBitrate = errors.New("bitrate bad or unset, defaulting") errGVBadVBRQuality = errors.New("VBR quality bad or unset, defaulting") errGVBadHeight = errors.New("height bad or unset, defaulting") errGVBadMinFrames = errors.New("min frames bad or unset, defaulting") ) // GeoVision is an implementation of the AVDevice interface for a GeoVision // IP camera. This has been designed to implement the GV-BX4700-8F in particular. // Any other models are untested. type GeoVision struct { cfg Config log Logger rtpClt *rtp.Client rtspClt *rtsp.Client rtcpClt *rtcp.Client } // NewGeoVision returns a new GeoVision. func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary using gvctrl to control the // GeoVision web interface. If fields are not valid, an error is added to the // multiError and a default value is used for that particular field. func (g *GeoVision) Set(c Config) error { var errs multiError if c.CameraIP == "" { errs = append(errs, errGVBadCameraIP) c.CameraIP = defaultGVCameraIP } switch c.InputCodec { case codecutil.H264, codecutil.H265, codecutil.MJPEG: default: errs = append(errs, errGVBadCodec) c.InputCodec = defaultGVCodec } if c.Height <= 0 { errs = append(errs, errGVBadHeight) c.Height = defaultGVHeight } if c.FrameRate <= 0 { errs = append(errs, errGVBadFrameRate) c.FrameRate = defaultGVFrameRate } if c.Bitrate <= 0 { errs = append(errs, errGVBadBitrate) c.Bitrate = defaultGVBitrate } if c.MinFrames <= 0 { errs = append(errs, errGVBadMinFrames) c.MinFrames = defaultGVMinFrames } switch c.VBRQuality { case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: default: errs = append(errs, errGVBadVBRQuality) c.VBRQuality = defaultVBRQuality } if c.VBRBitrate <= 0 { errs = append(errs, errGVBadVBRQuality) c.VBRBitrate = defaultVBRBitrate } if c.CameraChan != 1 && c.CameraChan != 2 { errs = append(errs, errGVBadVBRQuality) c.CameraChan = defaultCameraChan } g.cfg = c err := gvctrl.Set( g.cfg.CameraIP, gvctrl.Channel(g.cfg.CameraChan), gvctrl.CodecOut( map[uint8]gvctrl.Codec{ codecutil.H264: gvctrl.CodecH264, codecutil.H265: gvctrl.CodecH265, codecutil.MJPEG: gvctrl.CodecMJPEG, }[g.cfg.InputCodec], ), gvctrl.Height(int(g.cfg.Height)), gvctrl.FrameRate(int(g.cfg.FrameRate)), gvctrl.VariableBitrate(g.cfg.VBR), gvctrl.VBRQuality( map[quality]gvctrl.Quality{ qualityStandard: gvctrl.QualityStandard, qualityFair: gvctrl.QualityFair, qualityGood: gvctrl.QualityGood, qualityGreat: gvctrl.QualityGreat, qualityExcellent: gvctrl.QualityExcellent, }[g.cfg.VBRQuality], ), gvctrl.VBRBitrate(g.cfg.VBRBitrate), gvctrl.CBRBitrate(int(g.cfg.Bitrate)), gvctrl.Refresh(float64(g.cfg.MinFrames)/float64(g.cfg.FrameRate)), ) if err != nil { return fmt.Errorf("could not set IPCamera settings: %w", err) } // Give the camera some time to change it's configuration. const setupDelay = 5 * time.Second time.Sleep(setupDelay) return multiError(errs) } // Start uses an RTSP client to communicate with the GeoVision RTSP server and // request a stream that is then received by an RTP client, from which packets // can be read from using the Read method. func (g *GeoVision) Start() error { var ( local, remote *net.TCPAddr err error ) g.rtspClt, local, remote, err = rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp") if err != nil { return fmt.Errorf("could not create RTSP client: %w", err) } g.log.Log(logger.Info, pkg+"created RTSP client") resp, err := g.rtspClt.Options() if err != nil { return fmt.Errorf("options request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String()) resp, err = g.rtspClt.Describe() if err != nil { return fmt.Errorf("describe request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String()) resp, err = g.rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort)) if err != nil { return fmt.Errorf("setup request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String()) rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp) if err != nil { return fmt.Errorf("could not format addresses: %w", err) } g.log.Log(logger.Info, pkg+"RTSP session setup complete") g.rtpClt, err = rtp.NewClient(rtpCltAddr) if err != nil { return fmt.Errorf("could not create RTP client: %w", err) } g.rtcpClt, err = rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, g.rtpClt, g.log.Log) if err != nil { return fmt.Errorf("could not create RTCP client: %w", err) } g.log.Log(logger.Info, pkg+"RTCP and RTP clients created") // Check errors from RTCP client until it has stopped running. go func() { for { err, ok := <-g.rtcpClt.Err() if ok { g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error()) } else { return } } }() // Start the RTCP client. g.rtcpClt.Start() g.log.Log(logger.Info, pkg+"RTCP client started") resp, err = g.rtspClt.Play() if err != nil { return fmt.Errorf("play request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String()) g.log.Log(logger.Info, pkg+"play requested, now receiving stream") return nil } // Stop will close the RTSP, RTCP, and RTP connections and in turn end the // stream from the GeoVision. Future reads using Read will result in error. func (g *GeoVision) Stop() error { err := g.rtpClt.Close() if err != nil { return fmt.Errorf("could not close RTP client: %w", err) } err = g.rtspClt.Close() if err != nil { return fmt.Errorf("could not close RTSP client: %w", err) } g.rtcpClt.Stop() g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed") return nil } // Read implements io.Reader. If the GeoVision has not been started an error is // returned. func (g *GeoVision) Read(p []byte) (int, error) { if g.rtpClt != nil { return g.rtpClt.Read(p) } return 0, errors.New("cannot read, GeoVision not streaming") }