/* NAME revid-cli - command line interface for Revid. DESCRIPTION See Readme.md AUTHORS Saxon A. Nelson-Milton Jack Richardson LICENSE revid-cli is Copyright (C) 2017-2018 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 along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. */ package main import ( "flag" "fmt" "log" "os" "runtime/pprof" "strconv" "time" "unicode" "bitbucket.org/ausocean/av/revid" "bitbucket.org/ausocean/iot/pi/netsender" "bitbucket.org/ausocean/utils/smartlogger" ) const ( // progName is the program name for logging purposes. progName = "revid-cli" // Logging is set to INFO level. loggerVerbosity = 3 ) // Indexes for configFlags const ( inputPtr = iota inputCodecPtr outputPtr rtmpMethodPtr packetizationPtr quantizationModePtr verbosityPtr framesPerClipPtr rtmpUrlPtr bitratePtr outputFileNamePtr inputFileNamePtr heightPtr widthPtr frameRatePtr httpAddressPtr quantizationPtr timeoutPtr intraRefreshPeriodPtr verticalFlipPtr horizontalFlipPtr noOfConfigFlags ) // Other misc consts const ( netSendRetryTime = 5 * time.Second defaultRunDuration = 24 * time.Hour revidStopTime = 5 * time.Second ) // canProfile is set to false with revid-cli is built with "-tags profile". var canProfile = true // Globals var ( rv *revid.Revid config revid.Config ) func main() { cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") flagNames := [noOfConfigFlags]struct{ name, description string }{ {"Input", "The input type"}, {"InputCodec", "The codec of the input"}, {"Output", "The output type"}, {"RtmpMethod", "The method used to send over rtmp (ffmpeg or librtmp)"}, {"Packetization", "The method of data packetisation"}, {"QuantizationMode", "The level of quantization"}, {"Verbosity", "Verbosity on or off"}, {"FramesPerClip", "Number of frames per clip sent"}, {"RtmpUrl", "Url of rtmp endpoint"}, {"Bitrate", "Bitrate of recorded video"}, {"OutputFileName", "The directory of the output file"}, {"InputFileName", "The directory of the input file"}, {"Height", "Height in pixels"}, {"Width", "Width in pixels"}, {"FrameRate", "Frame rate of captured video"}, {"HttpAddress", "Destination address of http posts"}, {"Quantization", "Desired quantization value"}, {"Timeout", "Http timeout in seconds"}, {"IntraRefreshPeriod", "The IntraRefreshPeriod i.e. how many keyframes we send"}, {"VerticalFlip", "Flip video vertically"}, {"HorizontalFlip", "Flip video horizontally"}, } // Create the configFlags based on the flagNames array configFlags := make([](*string), noOfConfigFlags) for i, f := range &flagNames { configFlags[i] = flag.String(f.name, "", f.description) } // Do we want a netsender session useNetsender := flag.Bool("NetSender", false, "Are we checking vars through netsender?") // User might also want to define how long revid runs for runDurationPtr := flag.Duration("runDuration", defaultRunDuration, "How long do you want revid to run for?") flag.Parse() if *cpuprofile != "" { if canProfile { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal("could not create CPU profile: ", err) } if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("could not start CPU profile: ", err) } defer pprof.StopCPUProfile() } else { fmt.Fprintln(os.Stderr, "Ignoring cpuprofile flag - http/pprof built in.") } } switch *configFlags[inputPtr] { case "Raspivid": config.Input = revid.Raspivid case "File": config.Input = revid.File case "": default: fmt.Println("Bad input argument!") } switch *configFlags[inputCodecPtr] { case "H264Codec": config.InputCodec = revid.H264Codec case "": default: fmt.Println("Bad input codec argument!") } switch *configFlags[outputPtr] { case "File": config.Output = revid.File case "Http": config.Output = revid.Http case "Rtmp": config.Output = revid.Rtmp case "FfmpegRtmp": config.Output = revid.FfmpegRtmp case "": default: fmt.Println("Bad output argument!") } switch *configFlags[rtmpMethodPtr] { case "Ffmpeg": config.RtmpMethod = revid.Ffmpeg case "LibRtmp": config.RtmpMethod = revid.LibRtmp case "": default: fmt.Println("Bad rtmp method argument!") } switch *configFlags[packetizationPtr] { case "None": config.Packetization = revid.None case "Mpegts": config.Packetization = revid.Mpegts case "Flv": config.Packetization = revid.Flv case "": default: fmt.Println("Bad packetization argument!") } switch *configFlags[quantizationModePtr] { case "QuantizationOn": config.QuantizationMode = revid.QuantizationOn case "QuantizationOff": config.QuantizationMode = revid.QuantizationOff case "": default: fmt.Println("Bad quantization mode argument!") } switch *configFlags[verbosityPtr] { case "No": config.Verbosity = revid.No case "Yes": config.Verbosity = revid.Yes case "": default: fmt.Println("Bad verbosity argument!") } switch *configFlags[horizontalFlipPtr] { case "No": config.HorizontalFlip = revid.No case "Yes": config.HorizontalFlip = revid.Yes case "": config.HorizontalFlip = revid.No default: fmt.Println("Bad horizontal flip option!") } switch *configFlags[verticalFlipPtr] { case "No": config.VerticalFlip = revid.No case "Yes": config.VerticalFlip = revid.Yes case "": config.VerticalFlip = revid.No default: fmt.Println("Bad vertical flip option!") } fpc, err := strconv.Atoi(*configFlags[framesPerClipPtr]) if err == nil && fpc > 0 { config.FramesPerClip = fpc } config.RtmpUrl = *configFlags[rtmpUrlPtr] config.Bitrate = *configFlags[bitratePtr] config.OutputFileName = *configFlags[outputFileNamePtr] config.InputFileName = *configFlags[inputFileNamePtr] config.Height = *configFlags[heightPtr] config.Width = *configFlags[widthPtr] config.FrameRate = *configFlags[frameRatePtr] config.HttpAddress = *configFlags[httpAddressPtr] config.Quantization = *configFlags[quantizationPtr] config.Timeout = *configFlags[timeoutPtr] config.IntraRefreshPeriod = *configFlags[intraRefreshPeriodPtr] if !*useNetsender { // instantiate our own logger config.Logger = smartlogger.New(loggerVerbosity, smartlogger.File, "/var/log/netsender/") // run revid for the specified duration startRevid(nil) time.Sleep(*runDurationPtr) stopRevid() return } err = run() if err != nil { config.Logger.Log(progName, "Error", err.Error()) // TODO(kortschak): Make this "Fatal" when that exists. os.Exit(1) } } // initialize then run the main NetSender client func run() error { // initialize NetSender and use NetSender's logger config.Logger = netsender.Logger() config.Logger.Log(progName, "Info", "Running in NetSender mode") var ns netsender.Sender err := ns.Init(nil, nil, nil) if err != nil { return err } vars, _ := ns.Vars() vs := ns.VarSum() paused := false if vars["mode"] == "Paused" { paused = true } if !paused { err = updateRevid(&ns, vars, false) if err != nil { return err } } for { if err := send(&ns); err != nil { config.Logger.Log(progName, "Warning", err.Error()) time.Sleep(netSendRetryTime) continue } if vs != ns.VarSum() { // vars changed vars, err := ns.Vars() if err != nil { config.Logger.Log(progName, "Warning", err.Error()) time.Sleep(netSendRetryTime) continue } vs = ns.VarSum() if vars["mode"] == "Paused" { if !paused { config.Logger.Log(progName, "Info", "Pausing revid") stopRevid() paused = true } } else { err = updateRevid(&ns, vars, !paused) if err != nil { return err } if paused { paused = false } } } sleepTime, _ := strconv.Atoi(ns.Param("mp")) time.Sleep(time.Duration(sleepTime) * time.Second) } } // send implements our main NetSender client and handles NetReceiver configuration // (as distinct from httpSender which just sends video data). func send(ns *netsender.Sender) error { // populate input values, if any inputs := netsender.MakePins(ns.Param("ip"), "X") if rv != nil { for i, pin := range inputs { if pin.Name == "X23" { inputs[i].Value = rv.Bitrate() } } } _, reconfig, err := ns.Send(netsender.RequestPoll, inputs) if err != nil { return err } if reconfig { return ns.Config() } return nil } // wrappers for stopping and starting revid func startRevid(ns *netsender.Sender) (err error) { rv, err = revid.New(config, ns) if err != nil { return err } rv.Start() return nil } func stopRevid() { rv.Stop() // FIXME(kortschak): Is this waiting on completion of work? // Use a wait group and Wait method if it is. time.Sleep(revidStopTime) } func updateRevid(ns *netsender.Sender, vars map[string]string, stop bool) error { if stop { stopRevid() } //look through the vars and update revid where needed for key, value := range vars { switch key { case "Output": switch value { case "File": config.Output = revid.File case "Http": config.Output = revid.Http case "Rtmp": config.Output = revid.Rtmp case "FfmpegRtmp": config.Output = revid.FfmpegRtmp default: rv.Log(revid.Warning, "Invalid Output param: "+value) continue } case "FramesPerClip": fpc, err := strconv.Atoi(value) if fpc > 0 && err == nil { config.FramesPerClip = fpc } else { rv.Log(revid.Warning, "Invalid FramesPerClip param: "+value) } case "RtmpUrl": config.RtmpUrl = value case "Bitrate": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.Bitrate = value } else { rv.Log(revid.Warning, "Invalid Bitrate param: "+value) } case "OutputFileName": config.OutputFileName = value case "InputFileName": config.InputFileName = value case "Height": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.Height = value } else { rv.Log(revid.Warning, "Invalid Height param: "+value) } case "Width": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.Width = value } else { rv.Log(revid.Warning, "Invalid Width param: "+value) } case "FrameRate": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.FrameRate = value } else { rv.Log(revid.Warning, "Invalid FrameRate param: "+value) } case "HttpAddress": config.HttpAddress = value case "Quantization": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.Quantization = value } else { rv.Log(revid.Warning, "Invalid Quantization param: "+value) } case "Timeout": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.Timeout = value } case "IntraRefreshPeriod": asInt, err := strconv.Atoi(value) if asInt > 0 && err == nil { config.IntraRefreshPeriod = value } case "HorizontalFlip": switch value { case "Yes": config.HorizontalFlip = revid.Yes case "No": config.HorizontalFlip = revid.No default: rv.Log(revid.Warning, "Invalid HorizontalFlip param: "+value) } case "VerticalFlip": switch value { case "Yes": config.VerticalFlip = revid.Yes case "No": config.VerticalFlip = revid.No default: rv.Log(revid.Warning, "Invalid VerticalFlip param: "+value) } default: if unicode.IsUpper(rune(key[0])) { rv.Log(revid.Warning, "Unexpected param: "+key) } // else system params are lower case } } return startRevid(ns) }