/* NAME Revid.go DESCRIPTION See Readme.md AUTHORS Saxon A. Nelson-Milton Alan Noble LICENSE revid 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. */ // revid is a testbed for re-muxing and re-directing video streams as MPEG-TS over various protocols. package revid import ( "bufio" "bytes" "errors" "fmt" "io" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "strconv" "sync" "time" "bitbucket.org/ausocean/av/generator" "bitbucket.org/ausocean/av/parser" "bitbucket.org/ausocean/av/rtmp" "bitbucket.org/ausocean/utils/ring" ) // Misc constants const ( clipDuration = 1 * time.Second mp2tPacketSize = 188 // MPEG-TS packet size mp2tMaxPackets = int(clipDuration * 2016 / time.Second) // # first multiple of 7 and 8 greater than 2000 ringBufferSize = 500 ringBufferElementSize = 150000 writeTimeout = 10 * time.Millisecond readTimeout = 10 * time.Millisecond httpTimeout = 5 * time.Second packetsPerFrame = 7 bitrateTime = 1 * time.Minute mjpegParserInChanLen = 100000 ffmpegPath = "/usr/local/bin/ffmpeg" rtmpConnectionTimout = 10 outputChanSize = 1000 cameraRetryPeriod = 5 * time.Second sendFailedDelay = 5 maxSendFailedErrorCount = 500 clipSizeThreshold = 11 rtmpConnectionMaxTries = 5 raspividNoOfTries = 3 sendingWaitTime = 5 * time.Millisecond ) // Log Types const ( Error = "Error" Warning = "Warning" Info = "Info" Debug = "Debug" Detail = "Detail" ) // Revid provides methods to control a revid session; providing methods // to start, stop and change the state of an instance using the Config struct. type Revid struct { ffmpegPath string tempDir string ringBuffer *ring.Buffer config Config isRunning bool outputFile *os.File inputFile *os.File generator generator.Generator parser parser.Parser cmd *exec.Cmd ffmpegCmd *exec.Cmd inputReader *bufio.Reader ffmpegStdin io.WriteCloser outputChan chan []byte setupInput func() error setupOutput func() error getFrame func() []byte sendClip func(*ring.Chunk) error rtmpInst rtmp.Session mutex sync.Mutex sendMutex sync.Mutex currentBitrate int64 } // NewRevid returns a pointer to a new Revid with the desired // configuration, and/or an error if construction of the new instant was not // successful. func New(c Config) (*Revid, error) { var r Revid err := r.reset(c) if err != nil { return nil, err } r.ringBuffer = ring.NewBuffer(ringBufferSize, ringBufferElementSize, writeTimeout) r.outputChan = make(chan []byte, outputChanSize) return &r, nil } // Returns the currently saved bitrate from the most recent bitrate check // check bitrate output delay in consts for this period func (r *Revid) GetBitrate() int64 { return r.currentBitrate } // GetConfigRef returns a pointer to the revidInst's Config struct object func (r *Revid) GetConfigRef() *Config { return &r.config } // reset swaps the current config of a Revid with the passed // configuration; checking validity and returning errors if not valid. func (r *Revid) reset(config Config) error { r.config.Logger = config.Logger err := config.Validate(r) if err != nil { return errors.New("Config struct is bad!: " + err.Error()) } r.config = config switch r.config.Output { case File: r.setupOutput = r.setupOutputForFile r.sendClip = r.sendClipToFile case FfmpegRtmp: r.setupOutput = r.setupOutputForFfmpegRtmp r.sendClip = r.sendClipToFfmpegRtmp case NativeRtmp: r.setupOutput = r.setupOutputForLibRtmp r.sendClip = r.sendClipToLibRtmp case Http: r.sendClip = r.sendClipToHTTP } switch r.config.Input { case Raspivid: r.setupInput = r.setupInputForRaspivid case File: r.setupInput = r.setupInputForFile } switch r.config.InputCodec { case H264: r.Log(Info, "Using H264 parser!") r.parser = parser.NewH264Parser() case Mjpeg: r.Log(Info, "Using MJPEG parser!") r.parser = parser.NewMJPEGParser(mjpegParserInChanLen) } switch r.config.Packetization { case None: // no packetisation - Revid output chan grabs raw data straight from parser r.parser.SetOutputChan(r.outputChan) r.getFrame = r.getFrameNoPacketization goto noPacketizationSetup case Mpegts: r.Log(Info, "Using MPEGTS packetisation!") frameRateAsInt, _ := strconv.Atoi(r.config.FrameRate) r.generator = generator.NewTsGenerator(uint(frameRateAsInt)) case Flv: r.Log(Info, "Using FLV packetisation!") frameRateAsInt, _ := strconv.Atoi(r.config.FrameRate) r.generator = generator.NewFlvGenerator(true, true, uint(frameRateAsInt)) } // We have packetization of some sort, so we want to send data to Generator // to perform packetization r.getFrame = r.getFramePacketization r.parser.SetOutputChan(r.generator.GetInputChan()) noPacketizationSetup: return nil } // ChangeConfig changes the current configuration of the Revid instance. func (r *Revid) ChangeConfig(c Config) error { // FIXME(kortschak): This is reimplemented in cmd/revid-cli/main.go. // The implementation in the command is used and this is not. // Decide on one or the other. r.Stop() r, err := New(c) if err != nil { return err } r.Start() return err } // Log takes a logtype and message and tries to send this information to the // logger provided in the revid config - if there is one, otherwise the message // is sent to stdout func (r *Revid) Log(logType, m string) { if r.config.Verbosity == Yes { if r.config.Logger != nil { r.config.Logger.Log("revid", logType, m) return } fmt.Println(logType + ": " + m) } } // IsRunning returns true if the revid is currently running and false otherwise func (r *Revid) IsRunning() bool { return r.isRunning } // Start invokes a Revid to start processing video from a defined input // and packetising (if theres packetization) to a defined output. func (r *Revid) Start() { r.mutex.Lock() defer r.mutex.Unlock() if r.isRunning { r.Log(Warning, "Revid.Start() called but revid already running!") return } r.Log(Info, "Starting Revid!") r.Log(Debug, "Setting up output!") if r.setupOutput != nil { err := r.setupOutput() if err != nil { r.Log(Error, err.Error()) return } } r.isRunning = true r.Log(Info, "Starting output routine!") go r.outputClips() r.Log(Info, "Starting clip packing routine!") go r.packClips() r.Log(Info, "Starting packetisation generator!") r.generator.Start() r.Log(Info, "Starting parser!") r.parser.Start() r.Log(Info, "Setting up input and receiving content!") go r.setupInput() } // Stop halts any processing of video data from a camera or file func (r *Revid) Stop() { r.mutex.Lock() defer r.mutex.Unlock() if !r.isRunning { r.Log(Warning, "Revid.Stop() called but revid not running!") return } r.Log(Info, "Stopping revid!") // wait for sending to finish r.sendMutex.Lock() defer r.sendMutex.Unlock() r.rtmpInst.Close() r.isRunning = false r.Log(Info, "Stopping generator!") if r.generator != nil { r.generator.Stop() } r.Log(Info, "Stopping parser!") if r.parser != nil { r.parser.Stop() } r.Log(Info, "Killing input proccess!") // If a cmd process is running, we kill! if r.cmd != nil && r.cmd.Process != nil { r.cmd.Process.Kill() } } // getFrameNoPacketization gets a frame directly from the revid output chan // as we don't need to go through the generator with no packetization settings func (r *Revid) getFrameNoPacketization() []byte { return <-r.outputChan } // getFramePacketization gets a frame from the generators output chan - the // the generator being an mpegts or flv generator depending on the config func (r *Revid) getFramePacketization() []byte { return <-(r.generator.GetOutputChan()) } // flushDataPacketization removes data from the Revid inst's coutput chan func (r *Revid) flushData() { switch r.config.Packetization { case Flv: for { select { case <-(r.generator.GetOutputChan()): default: goto done } } } done: } // packClips takes data segments; whether that be tsPackets or mjpeg frames and // packs them into clips consisting of the amount frames specified in the config func (r *Revid) packClips() { clipSize := 0 packetCount := 0 for r.isRunning { select { // TODO: This is temporary, need to work out how to make this work // for cases when there is not packetisation. case frame := <-r.generator.GetOutputChan(): lenOfFrame := len(frame) if lenOfFrame > ringBufferElementSize { r.Log(Warning, fmt.Sprintf("Frame was too big: %v bytes, getting another one!", lenOfFrame)) frame = r.getFrame() lenOfFrame = len(frame) } _, err := r.ringBuffer.Write(frame) if err != nil { r.Log(Error, err.Error()) if err == ring.ErrDropped { r.Log(Warning, fmt.Sprintf("dropped %d byte frame", len(frame))) } } packetCount++ clipSize += lenOfFrame fpcAsInt, err := strconv.Atoi(r.config.FramesPerClip) if err != nil { r.Log(Error, "Frames per clip not quite right! Defaulting to 1!") r.config.FramesPerClip = "1" } if packetCount >= fpcAsInt { r.ringBuffer.Flush() clipSize = 0 packetCount = 0 continue } default: time.Sleep(time.Duration(5) * time.Millisecond) } } } // outputClips takes the clips produced in the packClips method and outputs them // to the desired output defined in the revid config func (r *Revid) outputClips() { now := time.Now() prevTime := now bytes := 0 delay := 0 for r.isRunning { // Here we slow things down as much as we can to decrease cpu usage switch { case r.ringBuffer.Len() < 2: delay++ time.Sleep(time.Duration(delay) * time.Millisecond) case delay > 0: delay-- } // If the ringbuffer has something we can read and send off chunk, err := r.ringBuffer.Next(readTimeout) if err != nil || !r.isRunning { if err == io.EOF { break } continue } bytes += chunk.Len() r.Log(Detail, "About to send") err = r.sendClip(chunk) if err == nil { r.Log(Detail, "Sent clip") } if r.isRunning && err != nil && chunk.Len() > 11 { r.Log(Debug, "Send failed! Trying again") // Try and send again err = r.sendClip(chunk) // if there's still an error we try and reconnect, unless we're stopping for r.isRunning && err != nil { r.Log(Debug, "Send failed a again! Trying to reconnect...") time.Sleep(time.Duration(sendFailedDelay) * time.Millisecond) r.Log(Error, err.Error()) if r.config.Output == NativeRtmp { r.Log(Debug, "Ending current rtmp session...") r.rtmpInst.Close() } if r.config.Output == NativeRtmp { r.Log(Info, "Restarting rtmp session...") r.rtmpInst.StartSession() } r.Log(Debug, "Trying to send again with new connection...") r.sendClip(chunk) // TODO(kortschak): Log these errors? } } chunk.Close() // ring.Chunk is an io.Closer, but Close alwats returns nil. r.Log(Detail, "Done reading that clip from ringbuffer...") // Log some information regarding bitrate and ring buffer size if it's time now = time.Now() deltaTime := now.Sub(prevTime) if deltaTime > bitrateTime { r.currentBitrate = int64(float64(bytes*8) / float64(deltaTime/1e9)) r.Log(Debug, fmt.Sprintf("Bitrate: %v bits/s\n", r.currentBitrate)) r.Log(Debug, fmt.Sprintf("Ring buffer size: %v\n", r.ringBuffer.Len())) prevTime = now bytes = 0 } } r.Log(Info, "Not outputting clips anymore!") } // senClipToFile writes the passed clip to a file func (r *Revid) sendClipToFile(clip *ring.Chunk) error { r.sendMutex.Lock() _, err := clip.WriteTo(r.outputFile) r.sendMutex.Unlock() return err } // sendClipToHTTP takes a clip and an output url and posts through http. func (r *Revid) sendClipToHTTP(clip *ring.Chunk) error { defer r.sendMutex.Unlock() r.sendMutex.Lock() client := http.Client{Timeout: httpTimeout} url := r.config.HttpAddress + strconv.Itoa(clip.Len()) // FIXME(kortschak): This is necessary because Post takes // an io.Reader as a parameter and closes it if it is an // io.Closer (which *ring.Chunk is), ... and because we // use a method value for dispatching the sendClip work. // So to save work in this case, sendClip should be made // a proper method with a behaviour switch based on a // Revid field so that we can prepare these bytes only // once for each clip (reusing a buffer field? or tt // might be work using a sync.Pool for the bodies). post := bytes.NewBuffer(make([]byte, 0, clip.Len())) _, err := clip.WriteTo(post) if err != nil { return fmt.Errorf("Error buffering: %v", err) } r.Log(Debug, fmt.Sprintf("Posting %s (%d bytes)\n", url, clip.Len())) resp, err := client.Post(url, "video/mp2t", post) if err != nil { return fmt.Errorf("Error posting to %s: %s", url, err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err == nil { r.Log(Debug, fmt.Sprintf("%s\n", body)) } else { r.Log(Error, err.Error()) } return nil } // sendClipToFfmpegRtmp sends the clip over the current rtmp connection using // an ffmpeg process. func (r *Revid) sendClipToFfmpegRtmp(clip *ring.Chunk) error { r.sendMutex.Lock() _, err := clip.WriteTo(r.ffmpegStdin) r.sendMutex.Unlock() return err } // sendClipToLibRtmp send the clip over the current rtmp connection using the // c based librtmp library func (r *Revid) sendClipToLibRtmp(clip *ring.Chunk) error { r.sendMutex.Lock() _, err := clip.WriteTo(r.rtmpInst) r.sendMutex.Unlock() return err } // setupOutputForFfmpegRtmp sets up output to rtmp using an ffmpeg process func (r *Revid) setupOutputForFfmpegRtmp() error { r.ffmpegCmd = exec.Command(ffmpegPath, "-f", "h264", "-r", r.config.FrameRate, "-i", "-", "-f", "lavfi", "-i", "aevalsrc=0", "-fflags", "nobuffer", "-vcodec", "copy", "-acodec", "aac", "-map", "0:0", "-map", "1:0", "-strict", "experimental", "-f", "flv", r.config.RtmpUrl, ) var err error r.ffmpegStdin, err = r.ffmpegCmd.StdinPipe() if err != nil { r.Log(Error, err.Error()) r.Stop() return err } err = r.ffmpegCmd.Start() if err != nil { r.Log(Error, err.Error()) r.Stop() return err } return nil } // setupOutputForLibRtmp sets up rtmp output using the wrapper for the c based // librtmp library - makes connection and starts comms etc. func (r *Revid) setupOutputForLibRtmp() error { r.rtmpInst = rtmp.NewSession(r.config.RtmpUrl, rtmpConnectionTimout) err := r.rtmpInst.StartSession() for noOfTries := 0; err != nil && noOfTries < rtmpConnectionMaxTries; noOfTries++ { r.rtmpInst.Close() r.Log(Error, err.Error()) r.Log(Info, "Trying to establish rtmp connection again!") r.rtmpInst = rtmp.NewSession(r.config.RtmpUrl, rtmpConnectionTimout) err = r.rtmpInst.StartSession() } if err != nil { return err } return err } // setupOutputForFile sets up an output file to output data to func (r *Revid) setupOutputForFile() (err error) { r.outputFile, err = os.Create(r.config.OutputFileName) return } // setupInputForRaspivid sets up things for input from raspivid i.e. starts // a raspivid process and pipes it's data output. func (r *Revid) setupInputForRaspivid() error { r.Log(Info, "Starting raspivid!") switch r.config.InputCodec { case H264: arguments := []string{"-cd", "H264", "-o", "-", "-n", "-t", r.config.Timeout, "-b", r.config.Bitrate, "-w", r.config.Width, "-h", r.config.Height, "-fps", r.config.FrameRate, "-ih", "-g", r.config.IntraRefreshPeriod, } if r.config.QuantizationMode == QuantizationOn { arguments = append(arguments, "-qp") arguments = append(arguments, r.config.Quantization) } if r.config.HorizontalFlip == Yes { arguments = append(arguments, "-hf") } if r.config.VerticalFlip == Yes { arguments = append(arguments, "-vf") } name := "raspivid" r.cmd = &exec.Cmd{ Path: name, Args: append([]string{name}, arguments...), } r.Log(Info, fmt.Sprintf("Startin raspivid with args: %v", r.cmd.Args)) if filepath.Base(name) == name { if lp, err := exec.LookPath(name); err != nil { r.Log(Error, err.Error()) return err } else { r.cmd.Path = lp } } case Mjpeg: r.cmd = exec.Command("raspivid", "-cd", "MJPEG", "-o", "-", "-n", "-t", r.config.Timeout, "-fps", r.config.FrameRate, ) } stdout, _ := r.cmd.StdoutPipe() go r.cmd.Run() r.inputReader = bufio.NewReader(stdout) go r.readCamera() return nil } // setupInputForFile sets things up for getting input from a file func (r *Revid) setupInputForFile() error { fps, _ := strconv.Atoi(r.config.FrameRate) r.parser.SetDelay(uint(float64(1000) / float64(fps))) r.readFile() return nil } // testRtmp is useful to check robustness of connections. Intended to be run as // goroutine. After every 'delayTime' the rtmp connection is ended and then // restarted func (r *Revid) testRtmp(delayTime uint) { for { time.Sleep(time.Duration(delayTime) * time.Millisecond) r.rtmpInst.Close() r.rtmpInst.StartSession() } } // readCamera reads data from the defined camera while the Revid is running. // TODO: use ringbuffer here instead of allocating mem every time! func (r *Revid) readCamera() { r.Log(Info, "Reading camera data!") for r.isRunning { data := make([]byte, 1) _, err := io.ReadFull(r.inputReader, data) switch { // We know this means we're getting nothing from the cam case (err != nil && err.Error() == "EOF" && r.isRunning) || (err != nil && r.isRunning): r.Log(Error, "No data from camera!") time.Sleep(cameraRetryPeriod) default: r.parser.GetInputChan() <- data[0] } } r.Log(Info, "Not trying to read from camera anymore!") } // readFile reads data from the defined file while the Revid is running. func (r *Revid) readFile() error { var err error r.inputFile, err = os.Open(r.config.InputFileName) if err != nil { r.Log(Error, err.Error()) r.Stop() return err } stats, err := r.inputFile.Stat() if err != nil { r.Log(Error, "Could not get input file stats!") r.Stop() return err } data := make([]byte, stats.Size()) _, err = r.inputFile.Read(data) if err != nil { r.Log(Error, err.Error()) r.Stop() return err } for i := range data { r.parser.GetInputChan() <- data[i] } r.inputFile.Close() return nil }