├── Dockerfile ├── config.yml └── main.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM squareone/fedora-gst-go 2 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | cameras: 2 | camera01: "rtsp://camera01.example.com:554/cam/realmonitor?channel=1&subtype=0" 3 | camera02: "rtsp://camera02.example.com:554/cam/realmonitor?channel=1&subtype=0" 4 | camera03: "rtsp://camera03.example.com:554/cam/realmonitor?channel=1&subtype=0" 5 | save_path: "/recorded-files/" 6 | scripts: 7 | record_start: "/local-bin/record_start.sh" 8 | record_end: "/local-bin/record_end.sh" -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pborman/uuid" 5 | "flag" 6 | "fmt" 7 | "github.com/ant0ine/go-json-rest/rest" 8 | "gopkg.in/yaml.v2" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "time" 14 | ) 15 | 16 | type Command string 17 | 18 | const ( 19 | START_RECORDING Command = "START_RECORDING" 20 | STOP_RECORDING Command = "STOP_RECORDING" 21 | GET_STATUS Command = "STATUS" 22 | ) 23 | 24 | type CameraStatus string 25 | 26 | const ( 27 | STATUS_RECORDING CameraStatus = "RECORDING" 28 | STATUS_NOTRECORDING CameraStatus = "NOT_RECORDING" 29 | ) 30 | 31 | type StatusPacket struct { 32 | Status CameraStatus 33 | } 34 | 35 | type CommandPacket struct { 36 | CameraId string 37 | Command Command 38 | StatusChan chan<- StatusPacket 39 | } 40 | 41 | type RecordingEntry struct { 42 | Process *os.Process 43 | Filename string 44 | } 45 | 46 | func process(commandChan <-chan CommandPacket, config Configuration) { 47 | 48 | go func() { 49 | currentProcs := map[string]RecordingEntry{} 50 | 51 | for pkt := range commandChan { 52 | if url, ok := config.Cameras[pkt.CameraId]; ok { 53 | if pkt.Command == START_RECORDING { 54 | if _, ok := currentProcs[pkt.CameraId]; !ok { 55 | filename := time.Now().UTC().Format("2006-01-02-15-04-05") + "_" + pkt.CameraId + "_" + uuid.New() + ".webm" 56 | saveLocation := config.SavePath + filename 57 | options := []string{ 58 | "-e", 59 | "rtspsrc", 60 | "location=" + url, 61 | "!", 62 | "rtph264depay", 63 | "!", 64 | "avdec_h264", 65 | "!", 66 | "queue", 67 | "!", 68 | "vp8enc", 69 | "deadline=100", 70 | "!", 71 | "webmmux", 72 | "!", 73 | "filesink", 74 | "location=" + saveLocation, 75 | } 76 | 77 | cmd := exec.Command("gst-launch-1.0", options...) 78 | if err := cmd.Start(); err == nil { 79 | currentProcs[pkt.CameraId] = RecordingEntry{ 80 | Process: cmd.Process, 81 | Filename: saveLocation, 82 | } 83 | 84 | if config.Scripts.RecordStart != "" { 85 | go func() { 86 | exec.Command(config.Scripts.RecordStart, pkt.CameraId, saveLocation).Run() 87 | }() 88 | } 89 | } else { 90 | fmt.Println(err) 91 | } 92 | } 93 | } 94 | 95 | if pkt.Command == STOP_RECORDING { 96 | if entry, ok := currentProcs[pkt.CameraId]; ok { 97 | go func(config Configuration, pkt CommandPacket, entry RecordingEntry) { 98 | entry.Process.Signal(os.Interrupt) 99 | entry.Process.Wait() 100 | 101 | if config.Scripts.RecordEnd != "" { 102 | go func() { 103 | exec.Command(config.Scripts.RecordEnd, pkt.CameraId, entry.Filename).Run() 104 | }() 105 | } 106 | }(config, pkt, entry) 107 | delete(currentProcs, pkt.CameraId) 108 | } 109 | } 110 | 111 | if pkt.Command == GET_STATUS { 112 | if _, ok := currentProcs[pkt.CameraId]; ok { 113 | pkt.StatusChan <- StatusPacket{ 114 | Status: STATUS_RECORDING, 115 | } 116 | } else { 117 | pkt.StatusChan <- StatusPacket{ 118 | Status: STATUS_NOTRECORDING, 119 | } 120 | } 121 | close(pkt.StatusChan) 122 | } 123 | } 124 | } 125 | }() 126 | } 127 | 128 | type Message struct { 129 | Body string 130 | } 131 | 132 | type Configuration struct { 133 | Cameras map[string]string 134 | SavePath string `yaml:"save_path"` 135 | Scripts struct { 136 | RecordStart string `yaml:"record_start"` 137 | RecordEnd string `yaml:"record_end"` 138 | } 139 | } 140 | 141 | func main() { 142 | var configLocation, listenOn string 143 | flag.StringVar(&configLocation, "config", "", "config location") 144 | flag.StringVar(&listenOn, "listen", ":8080", "IP/port to listen on") 145 | flag.Parse() 146 | 147 | configBytes, err := ioutil.ReadFile(configLocation) 148 | if err != nil { 149 | fmt.Println(err) 150 | os.Exit(1) 151 | } 152 | 153 | config := Configuration{} 154 | err = yaml.Unmarshal(configBytes, &config) 155 | if err != nil { 156 | fmt.Println(err) 157 | os.Exit(1) 158 | } 159 | 160 | commandChan := make(chan CommandPacket) 161 | 162 | process(commandChan, config) 163 | 164 | api := rest.NewApi() 165 | api.Use(rest.DefaultDevStack...) 166 | 167 | router, err := rest.MakeRouter( 168 | rest.Get("/cameras", func(w rest.ResponseWriter, req *rest.Request) { 169 | response := make([]string, len(config.Cameras)) 170 | idx := 0 171 | for c := range config.Cameras { 172 | response[idx] = c 173 | idx++ 174 | } 175 | w.WriteJson(response) 176 | }), 177 | 178 | rest.Get("/:camera/:action", func(w rest.ResponseWriter, req *rest.Request) { 179 | 180 | camera := req.PathParam("camera") 181 | 182 | if _, ok := config.Cameras[camera]; !ok { 183 | rest.Error(w, "camera not recognized", 400) 184 | return 185 | } 186 | 187 | action := req.PathParam("action") 188 | if action == "start" { 189 | commandChan <- CommandPacket{ 190 | CameraId: camera, 191 | Command: START_RECORDING, 192 | } 193 | w.WriteJson(&Message{Body: "OK"}) 194 | return 195 | } 196 | 197 | if action == "stop" { 198 | commandChan <- CommandPacket{ 199 | CameraId: camera, 200 | Command: STOP_RECORDING, 201 | } 202 | w.WriteJson(&Message{Body: "OK"}) 203 | return 204 | } 205 | 206 | if action == "status" { 207 | statusChan := make(chan StatusPacket) 208 | commandChan <- CommandPacket{ 209 | CameraId: camera, 210 | Command: GET_STATUS, 211 | StatusChan: statusChan, 212 | } 213 | 214 | status := <-statusChan 215 | w.WriteJson(&status) 216 | return 217 | } 218 | 219 | rest.Error(w, "action not recognized", 400) 220 | return 221 | }), 222 | ) 223 | 224 | if err != nil { 225 | fmt.Println(err) 226 | os.Exit(1) 227 | } 228 | 229 | api.SetApp(router) 230 | 231 | http.ListenAndServe(listenOn, api.MakeHandler()) 232 | } 233 | --------------------------------------------------------------------------------