/* NAME revid - a testbed for re-muxing and re-directing video streams as MPEG-TS over various protocols. DESCRIPTION See Readme.md AUTHORS Alan Noble Saxon A. Nelson-Milton LICENSE revid is Copyright (C) 2017 Alan Noble. 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 [GNU licenses](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" "fmt" "io" "io/ioutil" "net" "net/http" "os" "os/exec" "strconv" "time" "bitbucket.org/ausocean/av/h264" "bitbucket.org/ausocean/av/tsgenerator" "bitbucket.org/ausocean/av/ringbuffer" "bitbucket.org/ausocean/utils/smartLogger" ) // defaults and networking consts const ( clipDuration = 1 // s mp2tPacketSize = 188 // MPEG-TS packet size mp2tMaxPackets = 2016 * clipDuration // # first multiple of 7 and 8 greater than 2000 udpPackets = 7 // # of UDP packets per ethernet frame (8 is the max) rtpPackets = 7 // # of RTP packets per ethernet frame (7 is the max) rtpHeaderSize = 12 rtpSSRC = 1 // any value will do bufferSize = 100 / clipDuration httpTimeOut = 5 // s packetsPerFrame = 7 h264BufferSize = 500000 bitrateTime = 60 ) // Log Types const ( Error = "Error" Warning = "Warning" Info = "Info" Debug = "Debug" ) // Config enums const ( Raspivid = 0 Rtp = 1 H264Codec = 2 File = 4 HttpOut = 5 ) type Config struct { Input uint8 InputCmd string Output uint8 OutputFileName string InputFileName string Height string Width string Bitrate string FrameRate string HttpAddress string Quantization string Logger smartLogger.LogInstance } type RevidInst interface { Start() Stop() ChangeState(newconfig Config) error GetConfigRef() *Config Log(logType, m string) IsRunning() bool } type revidInst struct { dumpPCRBase uint64 conn net.Conn ffmpegPath string tempDir string ringBuffer ringbuffer.RingBuffer config Config isRunning bool outputFile *os.File inputFile *os.File generator tsgenerator.TsGenerator h264Parser h264.H264Parser cmd *exec.Cmd inputReader *bufio.Reader } func NewRevidInstance(config Config) (r *revidInst, err error) { r = new(revidInst) r.ringBuffer = ringbuffer.NewRingBuffer(bufferSize, mp2tPacketSize*mp2tMaxPackets) r.dumpPCRBase = 0 r.ChangeState(config) switch r.config.Output { case File: r.outputFile, err = os.Create(r.config.OutputFileName) if err != nil { return nil, err } } switch r.config.Input { case File: r.inputFile, err = os.Open(r.config.InputFileName) if err != nil { return nil, err } } r.generator = tsgenerator.NewTsGenerator(25) r.generator.Start() r.h264Parser = h264.H264Parser{OutputChan: r.generator.GetNalInputChan()} r.h264Parser.Start() go r.input() r.Log(Info, "New revid instance created! config is:") r.Log(Info, fmt.Sprintf("%v", r.config)) return } func (r *revidInst) GetConfigRef() *Config { return &r.config } func (r *revidInst) ChangeState(newconfig Config) error { // TODO: check that the config is legit r.config = newconfig return nil } func (r *revidInst) Log(logType, m string) { r.config.Logger.Log(logType, m) } func (r *revidInst) IsRunning() bool { return r.isRunning } func (r *revidInst) Start() { if r.isRunning { r.Log(Warning, "revidInst.Start() called but revid already running!") return } r.Log(Info, "Starting Revid!") var h264Data []byte switch r.config.Input { case Raspivid: r.Log(Info, "Starting raspivid!") r.cmd = exec.Command("raspivid", "-o", "-", "-n", "-t", "0", "-b", r.config.Bitrate, "-qp", r.config.Quantization, "-w", r.config.Width, "-h", r.config.Height, "-fps", r.config.FrameRate, "-ih", "-g", "100") stdout, _ := r.cmd.StdoutPipe() err := r.cmd.Start() r.inputReader = bufio.NewReader(stdout) if err != nil { r.Log(Error, err.Error()) return } r.isRunning = true go func() { r.Log(Info, "Reading camera data!") for r.isRunning { h264Data = make([]byte, 1) _, err := io.ReadFull(r.inputReader, h264Data) switch { case err != nil && err.Error() == "EOF" && r.isRunning: r.Log(Error, "No data from camera!") time.Sleep(5 * time.Second) case err != nil && r.isRunning: r.Log(Error, err.Error()) default: r.h264Parser.InputByteChan <- h264Data[0] } } r.Log(Info, "Out of reading routine!") }() case File: stats, err := r.inputFile.Stat() if err != nil { r.Log(Error, "Could not get input file stats!") r.Stop() return } h264Data = make([]byte, stats.Size()) _, err = r.inputFile.Read(h264Data) if err != nil { r.Log(Error, err.Error()) r.Stop() return } for i := range h264Data { r.h264Parser.InputByteChan <- h264Data[i] } } go r.output() } func (r *revidInst) Stop() { if r.isRunning { r.Log(Info, "Stopping revid!") r.isRunning = false r.cmd.Process.Kill() } else { r.Log(Warning, "revidInst.Stop() called but revid not running!") } } func (r *revidInst) input() { clipSize := 0 packetCount := 0 now := time.Now() prevTime := now for { if clip, err := r.ringBuffer.Get(); err != nil { r.Log(Error, err.Error()) r.Log(Warning, "Clearing TS chan!") for len(r.generator.GetTsOutputChan()) > 0 { <-(r.generator.GetTsOutputChan()) } time.Sleep(1 * time.Second) } else { for { tsPacket := <-(r.generator.GetTsOutputChan()) tsByteSlice, err := tsPacket.ToByteSlice() if err != nil { r.Log(Error, err.Error()) } upperBound := clipSize + mp2tPacketSize copy(clip[clipSize:upperBound], tsByteSlice) packetCount++ clipSize += mp2tPacketSize // send if (1) our buffer is full or (2) 1 second has elapsed and we have % packetsPerFrame now = time.Now() if (packetCount == mp2tMaxPackets) || (now.Sub(prevTime) > clipDuration*time.Second && packetCount%packetsPerFrame == 0) { if err := r.ringBuffer.DoneWriting(clipSize); err != nil { r.Log(Error, err.Error()) r.Log(Warning, "Dropping clip!") } clipSize = 0 packetCount = 0 prevTime = now break } } } } } func (r *revidInst) output() { now := time.Now() prevTime := now bytes := 0 delay := 0 for r.isRunning { switch { case r.ringBuffer.GetNoOfElements() < 2: delay++ time.Sleep(time.Duration(delay) * time.Millisecond) case delay > 10: delay -= 10 } if clip, err := r.ringBuffer.Read(); err == nil { r.Log(Debug, fmt.Sprintf("Delay is: %v\n", delay)) r.Log(Debug, fmt.Sprintf("Ring buffer size: %v\n", r.ringBuffer.GetNoOfElements())) switch r.config.Output { case File: r.outputFile.Write(clip) case HttpOut: bytes += len(clip) for err := r.sendClipToHTTP(clip, r.config.HttpAddress); err != nil; { r.Log(Error, err.Error()) r.Log(Warning, "Post failed trying again!") err = r.sendClipToHTTP(clip, r.config.HttpAddress) } default: r.Log(Error, "No output defined!") } if err := r.ringBuffer.DoneReading(); err != nil { r.Log(Error, err.Error()) } now = time.Now() deltaTime := now.Sub(prevTime) if deltaTime > time.Duration(bitrateTime)*time.Second { r.Log(Info, fmt.Sprintf("Bitrate: %v bits/s\n", int64(float64(bytes*8)/float64(deltaTime/1e9)))) r.Log(Info, fmt.Sprintf("Ring buffer size: %v\n", r.ringBuffer.GetNoOfElements())) prevTime = now bytes = 0 } } } } // sendClipToHTPP posts a video clip via HTTP, using a new TCP connection each time. func (r *revidInst) sendClipToHTTP(clip []byte, output string) error { timeout := time.Duration(httpTimeOut * time.Second) client := http.Client{ Timeout: timeout, } url := output + strconv.Itoa(len(clip)) r.Log(Debug, fmt.Sprintf("Posting %s (%d bytes)\n", url, len(clip))) resp, err := client.Post(url, "video/mp2t", bytes.NewReader(clip)) // lighter than NewBuffer if err != nil { return fmt.Errorf("Error posting to %s: %s", output, 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 }