├── .gitignore ├── README.md ├── main.go └── server ├── recorder ├── implementation.go ├── interface.go └── mock.go ├── server.go ├── storage ├── implementation.go └── interface.go └── util ├── keys.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | /pi-camera-go 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pi-camera-go 2 | ------------ 3 | 4 | This is a project to create a Go-based server for streaming video from the Raspberry Pi camera module. 5 | 6 | License 7 | ------- 8 | Copyright © 2018 Josh A. Beam 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 14 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "flag" 30 | "fmt" 31 | 32 | "github.com/joshb/pi-camera-go/server" 33 | ) 34 | 35 | func main() { 36 | address := flag.String("address", "localhost:10042", "The address (including port) to bind to") 37 | useHTTPS := flag.Bool("https", false, "Use HTTPS") 38 | flag.Parse() 39 | 40 | s, err := server.New(*useHTTPS) 41 | if err != nil { 42 | fmt.Println("Unable to create server:", err) 43 | return 44 | } 45 | 46 | if err := s.Start(*address); err != nil { 47 | fmt.Println("Unable to start server:", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/recorder/implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package recorder 27 | 28 | import ( 29 | "context" 30 | "fmt" 31 | "io/ioutil" 32 | "os" 33 | "os/exec" 34 | "path" 35 | "strconv" 36 | "strings" 37 | "time" 38 | 39 | "github.com/joshb/pi-camera-go/server/util" 40 | ) 41 | 42 | type recorderImpl struct { 43 | cancelFunc context.CancelFunc 44 | cmd *exec.Cmd 45 | 46 | recorderDir string 47 | segmentDuration time.Duration 48 | width int 49 | height int 50 | bitRate int 51 | 52 | subscribers []Subscriber 53 | } 54 | 55 | func New() (Recorder, error) { 56 | recorderDir, err := util.ConfigDir("recorder") 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return &recorderImpl{ 62 | recorderDir: recorderDir, 63 | segmentDuration: 5 * time.Second, 64 | width: 640, 65 | height: 480, 66 | bitRate: 4000000, 67 | }, nil 68 | } 69 | 70 | func (r *recorderImpl) muxFile(name string) (string, error) { 71 | t := time.Now() 72 | 73 | inPath := path.Join(r.recorderDir, name) 74 | newName := strings.Replace(name, ".h264", ".ts", 1) 75 | outPath := path.Join(r.recorderDir, newName) 76 | 77 | // Use ffmpeg to mux the file. 78 | args := []string{ 79 | "-i", inPath, 80 | "-codec", "copy", 81 | outPath, 82 | } 83 | cmd := exec.Command("ffmpeg", args...) 84 | if err := cmd.Run(); err != nil { 85 | return "", err 86 | } 87 | 88 | // Remove the input file. 89 | if err := os.Remove(inPath); err != nil { 90 | return "", err 91 | } 92 | 93 | d := time.Since(t) 94 | println("Created", outPath, "in", d / time.Millisecond, "ms") 95 | 96 | return outPath, nil 97 | } 98 | 99 | func (r *recorderImpl) deleteFiles() error { 100 | files, err := ioutil.ReadDir(r.recorderDir) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | for _, fileInfo := range files { 106 | if !strings.HasSuffix(fileInfo.Name(), ".h264") { 107 | continue 108 | } 109 | 110 | filePath := path.Join(r.recorderDir, fileInfo.Name()) 111 | if err := os.Remove(filePath); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (r *recorderImpl) checkFiles() error { 120 | allFiles, err := ioutil.ReadDir(r.recorderDir) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | // Build a list of .h264 files. 126 | files := make([]os.FileInfo, 0, len(allFiles)) 127 | for _, fileInfo := range allFiles { 128 | if strings.HasSuffix(fileInfo.Name(), ".h264") { 129 | files = append(files, fileInfo) 130 | } 131 | } 132 | 133 | // If there are less than two files, we have nothing to do. 134 | filesLen := len(files) 135 | if filesLen < 2 { 136 | return nil 137 | } 138 | 139 | // Notify subscribers of any new video files and then remove them. 140 | for _, fileInfo := range files[:filesLen-1] { 141 | filePath, err := r.muxFile(fileInfo.Name()) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | created := time.Now() 147 | modified := created.Add(r.segmentDuration) 148 | for _, subscriber := range r.subscribers { 149 | subscriber.VideoRecorded(filePath, created, modified) 150 | } 151 | 152 | // Remove the file. 153 | if err := os.Remove(filePath); err != nil { 154 | return err 155 | } 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func (r *recorderImpl) checkFilesLoop() { 162 | for r.cmd != nil { 163 | if err := r.checkFiles(); err != nil { 164 | fmt.Println("Error when checking files:", err) 165 | } 166 | 167 | time.Sleep(time.Second) 168 | } 169 | } 170 | 171 | func (r *recorderImpl) Start() error { 172 | if err := r.deleteFiles(); err != nil { 173 | return err 174 | } 175 | 176 | var ctx context.Context 177 | ctx, cancelFunc := context.WithCancel(context.Background()) 178 | segmentPath := path.Join(r.recorderDir, "segment%012d.h264") 179 | args := []string{ 180 | "--segment", strconv.Itoa(int(r.segmentDuration / time.Millisecond)), 181 | "--timeout", "0", 182 | "--width", strconv.Itoa(r.width), 183 | "--height", strconv.Itoa(r.height), 184 | "-b", strconv.Itoa(r.bitRate), 185 | "-o", segmentPath, 186 | } 187 | cmd := exec.CommandContext(ctx, "raspivid", args...) 188 | 189 | if err := cmd.Start(); err != nil { 190 | return err 191 | } 192 | 193 | r.cancelFunc = cancelFunc 194 | r.cmd = cmd 195 | 196 | go r.checkFilesLoop() 197 | return nil 198 | } 199 | 200 | func (r *recorderImpl) Stop() error { 201 | cancelFunc, cmd := r.cancelFunc, r.cmd 202 | r.cancelFunc, r.cmd = nil, nil 203 | cancelFunc() 204 | return cmd.Wait() 205 | } 206 | 207 | func (r *recorderImpl) SegmentDuration() time.Duration { 208 | return r.segmentDuration 209 | } 210 | 211 | func (r *recorderImpl) AddSubscriber(subscriber Subscriber) { 212 | r.subscribers = append(r.subscribers, subscriber) 213 | } -------------------------------------------------------------------------------- /server/recorder/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package recorder 27 | 28 | import ( 29 | "time" 30 | ) 31 | 32 | type Subscriber interface { 33 | VideoRecorded(filePath string, created, modified time.Time) 34 | } 35 | 36 | type Recorder interface { 37 | Start() error 38 | Stop() error 39 | 40 | SegmentDuration() time.Duration 41 | AddSubscriber(subscriber Subscriber) 42 | } 43 | -------------------------------------------------------------------------------- /server/recorder/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package recorder 27 | 28 | import ( 29 | "time" 30 | ) 31 | 32 | type mockRecorder struct { 33 | running bool 34 | subscribers []Subscriber 35 | } 36 | 37 | func NewMock() Recorder { 38 | return &mockRecorder{} 39 | } 40 | 41 | func (r *mockRecorder) Start() error { 42 | r.running = true 43 | return nil 44 | } 45 | 46 | func (r *mockRecorder) Stop() error { 47 | r.running = false 48 | return nil 49 | } 50 | 51 | func (r *mockRecorder) SegmentDuration() time.Duration { 52 | return 5 * time.Second 53 | } 54 | 55 | func (r *mockRecorder) AddSubscriber(subscriber Subscriber) { 56 | r.subscribers = append(r.subscribers, subscriber) 57 | } 58 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package server 27 | 28 | import ( 29 | "fmt" 30 | "io" 31 | "net/http" 32 | "strings" 33 | "time" 34 | 35 | "github.com/joshb/pi-camera-go/server/recorder" 36 | "github.com/joshb/pi-camera-go/server/storage" 37 | "github.com/joshb/pi-camera-go/server/util" 38 | ) 39 | 40 | const ( 41 | segmentsPrefix = "/segments/" 42 | staticPrefix = "/" 43 | ) 44 | 45 | type Server interface { 46 | Start(addr string) error 47 | Stop() error 48 | } 49 | 50 | type serverImpl struct { 51 | privateKeyPath string 52 | publicKeyPath string 53 | 54 | storage storage.Storage 55 | recorder recorder.Recorder 56 | 57 | segmentsFileServer http.Handler 58 | staticFileServer http.Handler 59 | } 60 | 61 | func New(https bool) (Server, error) { 62 | var privateKeyPath, publicKeyPath string 63 | if https { 64 | var err error 65 | privateKeyPath, publicKeyPath, err = util.KeyPaths() 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | return &serverImpl{ 72 | privateKeyPath: privateKeyPath, 73 | publicKeyPath: publicKeyPath, 74 | }, nil 75 | } 76 | 77 | func (s *serverImpl) Start(addr string) error { 78 | var err error 79 | s.storage, err = storage.New() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | s.segmentsFileServer = http.StripPrefix(segmentsPrefix, 85 | http.FileServer(http.Dir(s.storage.SegmentDir()))) 86 | s.staticFileServer = http.StripPrefix(staticPrefix, 87 | http.FileServer(http.Dir("static"))) 88 | 89 | s.recorder, err = recorder.New() 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if err := s.recorder.Start(); err != nil { 95 | fmt.Println("Unable to start recorder:", err) 96 | fmt.Println("Using mock recorder") 97 | s.recorder = recorder.NewMock() 98 | if err := s.recorder.Start(); err != nil { 99 | return err 100 | } 101 | } 102 | 103 | s.recorder.AddSubscriber(s.storage) 104 | 105 | println("Starting server at address", addr) 106 | if len(s.publicKeyPath) != 0 && len(s.privateKeyPath) != 0 { 107 | return http.ListenAndServeTLS(addr, s.publicKeyPath, s.privateKeyPath, s) 108 | } else { 109 | return http.ListenAndServe(addr, s) 110 | } 111 | } 112 | 113 | func (s *serverImpl) Stop() error { 114 | if err := s.recorder.Stop(); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (s *serverImpl) ServeHTTP(w http.ResponseWriter, req *http.Request) { 122 | u := req.URL.String() 123 | if strings.HasPrefix(u, segmentsPrefix) { 124 | s.segmentsFileServer.ServeHTTP(w, req) 125 | } else if u == "/live.m3u" { 126 | s.serveLivePlaylist(w, false) 127 | } else if u == "/live.txt" { 128 | s.serveLivePlaylist(w, true) 129 | } else { 130 | s.staticFileServer.ServeHTTP(w, req) 131 | } 132 | } 133 | 134 | func (s *serverImpl) serveLivePlaylist(w http.ResponseWriter, txt bool) { 135 | // Get enough segments to fill 10 seconds. 136 | numSegments := int((10 * time.Second) / s.recorder.SegmentDuration()) 137 | if numSegments < 3 { 138 | numSegments = 3 139 | } 140 | 141 | segments := s.storage.LatestSegments(numSegments) 142 | targetDuration := time.Duration(0) 143 | firstSegmentID := storage.SegmentID(0) 144 | for _, segment := range segments { 145 | if segment.Duration > targetDuration { 146 | targetDuration = segment.Duration 147 | } 148 | 149 | if firstSegmentID == 0 { 150 | firstSegmentID = segment.ID 151 | } 152 | } 153 | 154 | if txt { 155 | w.Header().Set("Content-Type", "text/plain") 156 | } else { 157 | w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 158 | } 159 | 160 | io.WriteString(w, "#EXTM3U\n") 161 | targetDurationInt := int(targetDuration / time.Second) 162 | io.WriteString(w, fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", targetDurationInt)) 163 | io.WriteString(w, fmt.Sprintf("#EXT-X-MEDIA-SEQUENCE:%d\n", firstSegmentID)) 164 | 165 | prevSegmentID := firstSegmentID - 1 166 | for _, segment := range segments { 167 | // Indicate if there is a gap in segments. 168 | if segment.ID != prevSegmentID + 1 { 169 | io.WriteString(w, "#EXT-X-DISCONTINUITY\n") 170 | } 171 | 172 | duration := float64(segment.Duration) / float64(time.Second) 173 | io.WriteString(w, fmt.Sprintf("#EXTINF:%f,\n", duration)) 174 | io.WriteString(w, fmt.Sprintf("segments/%s\n", segment.Name)) 175 | 176 | prevSegmentID = segment.ID 177 | } 178 | } -------------------------------------------------------------------------------- /server/storage/implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package storage 27 | 28 | import ( 29 | "fmt" 30 | "errors" 31 | "io" 32 | "io/ioutil" 33 | "os" 34 | "path" 35 | "strings" 36 | "strconv" 37 | "sync" 38 | "time" 39 | 40 | "github.com/joshb/pi-camera-go/server/util" 41 | ) 42 | 43 | type storageImpl struct { 44 | segmentDir string 45 | segmentDirMaxSize int64 46 | segments map[SegmentID]Segment 47 | lastSegmentID SegmentID 48 | mutex *sync.Mutex 49 | } 50 | 51 | func New() (Storage, error) { 52 | segmentDir, err := util.ConfigDir("segments") 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | segments, lastSegmentID, err := loadSegments(segmentDir) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return &storageImpl{ 63 | segmentDir: segmentDir, 64 | segmentDirMaxSize: 1024*1024*1024, // 1 GB 65 | segments: segments, 66 | lastSegmentID: lastSegmentID + 1, 67 | mutex: &sync.Mutex{}, 68 | }, nil 69 | } 70 | 71 | func (s *storageImpl) SegmentDir() string { 72 | return s.segmentDir 73 | } 74 | 75 | func loadSegments(segmentDir string) (map[SegmentID]Segment, SegmentID, error) { 76 | // Get a listing of files in the segment directory. 77 | files, err := ioutil.ReadDir(segmentDir) 78 | if err != nil { 79 | return nil, 0, err 80 | } 81 | 82 | // Build a map of segments. 83 | segments := make(map[SegmentID]Segment, len(files)) 84 | lastSegmentID := SegmentID(0) 85 | for _, fileInfo := range files { 86 | segment, err := segmentFromFileName(fileInfo.Name()) 87 | if err == nil { 88 | segments[segment.ID] = segment 89 | if segment.ID > lastSegmentID { 90 | lastSegmentID = segment.ID 91 | } 92 | } 93 | } 94 | 95 | return segments, lastSegmentID, nil 96 | } 97 | 98 | func segmentFromFileName(name string) (Segment, error) { 99 | parts := strings.Split(strings.Split(name, ".")[0], "_") 100 | if len(parts) != 4 || parts[0] != "segment" { 101 | return Segment{}, errors.New("invalid segment file name") 102 | } 103 | 104 | // Parse segment time. 105 | segmentTime, err := strconv.Atoi(parts[1]) 106 | if err != nil { 107 | return Segment{}, err 108 | } 109 | 110 | // Parse segment duration. 111 | segmentDuration, err := strconv.Atoi(parts[2]) 112 | if err != nil { 113 | return Segment{}, err 114 | } 115 | 116 | // Parse segment ID. 117 | segmentID, err := strconv.Atoi(parts[3]) 118 | if err != nil { 119 | return Segment{}, err 120 | } 121 | 122 | return Segment{ 123 | ID: SegmentID(segmentID), 124 | Name: name, 125 | Time: time.Unix(int64(segmentTime), 0), 126 | Duration: time.Duration(segmentDuration) * time.Millisecond, 127 | }, nil 128 | } 129 | 130 | func (s *storageImpl) LatestSegments(count int) []Segment { 131 | s.mutex.Lock() 132 | 133 | segments := make([]Segment, 0, count) 134 | lastSegmentID := s.lastSegmentID 135 | for segmentID := lastSegmentID - SegmentID(count) + 1; segmentID <= lastSegmentID; segmentID++ { 136 | if segment, ok := s.segments[segmentID]; ok { 137 | segments = append(segments, segment) 138 | } 139 | } 140 | 141 | s.mutex.Unlock() 142 | return segments 143 | } 144 | 145 | func (s *storageImpl) addSegment(filePath string, created, modified time.Time) error { 146 | t := time.Now() 147 | 148 | inFile, err := os.Open(filePath) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | fileInfo, err := inFile.Stat() 154 | if err != nil { 155 | return err 156 | } 157 | 158 | segmentTime := created 159 | segmentDuration := modified.Sub(created) 160 | 161 | // Generate segment file name/path. 162 | segmentID := s.lastSegmentID + 1 163 | segmentName := fmt.Sprintf("segment_%d_%d_%d.ts", segmentTime.Unix(), 164 | (segmentDuration / time.Millisecond), segmentID) 165 | segmentPath := path.Join(s.segmentDir, segmentName) 166 | 167 | // Copy the file to the segments directory. 168 | outFile, err := os.Create(segmentPath) 169 | if err != nil { 170 | return err 171 | } 172 | if n, err := io.Copy(outFile, inFile); err != nil { 173 | return err 174 | } else if n != fileInfo.Size() { 175 | return errors.New("could not copy entire file") 176 | } 177 | 178 | s.mutex.Lock() 179 | s.lastSegmentID = segmentID 180 | s.segments[segmentID] = Segment{ 181 | ID: segmentID, 182 | Name: segmentName, 183 | Time: segmentTime, 184 | Duration: segmentDuration, 185 | } 186 | s.mutex.Unlock() 187 | 188 | d := time.Since(t) 189 | println("Added segment", segmentID, "in", d / time.Millisecond, "ms") 190 | 191 | return nil 192 | } 193 | 194 | func (s *storageImpl) VideoRecorded(filePath string, created, modified time.Time) { 195 | if err := s.addSegment(filePath, created, modified); err != nil { 196 | fmt.Println("Error when adding segment:", err) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /server/storage/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package storage 27 | 28 | import ( 29 | "time" 30 | ) 31 | 32 | type SegmentID uint64 33 | 34 | type Segment struct { 35 | ID SegmentID 36 | Name string 37 | Time time.Time 38 | Duration time.Duration 39 | } 40 | 41 | type Storage interface { 42 | SegmentDir() string 43 | LatestSegments(count int) []Segment 44 | VideoRecorded(filePath string, created, modified time.Time) 45 | } 46 | -------------------------------------------------------------------------------- /server/util/keys.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package util 27 | 28 | import ( 29 | "crypto/rand" 30 | "crypto/rsa" 31 | "crypto/x509" 32 | "crypto/x509/pkix" 33 | "encoding/pem" 34 | "math/big" 35 | "os" 36 | "path" 37 | "time" 38 | ) 39 | 40 | func KeyPaths() (string, string, error) { 41 | keyDir, err := ConfigDir("keys") 42 | if err != nil { 43 | return "", "", err 44 | } 45 | 46 | privateKeyPath := path.Join(keyDir, "private.pem") 47 | _, err = os.Stat(privateKeyPath) 48 | privateKeyExists := err == nil 49 | 50 | publicKeyPath := path.Join(keyDir, "public.pem") 51 | _, err = os.Stat(publicKeyPath) 52 | publicKeyExists := err == nil 53 | 54 | if !privateKeyExists || !publicKeyExists { 55 | if err := createKeys(privateKeyPath, publicKeyPath); err != nil { 56 | return "", "", err 57 | } 58 | } 59 | 60 | return privateKeyPath, publicKeyPath, nil 61 | } 62 | 63 | func createKeys(privateKeyPath, publicKeyPath string) error { 64 | serialNumMax := (&big.Int{}).Lsh(big.NewInt(1), 256) 65 | serialNum, err := rand.Int(rand.Reader, serialNumMax) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | startTime := time.Now() 71 | endTime := startTime.AddDate(1, 0, 0) 72 | 73 | cert := x509.Certificate{ 74 | BasicConstraintsValid: true, 75 | KeyUsage: x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature, 76 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 77 | NotBefore: startTime, 78 | NotAfter: endTime, 79 | SerialNumber: serialNum, 80 | Subject: pkix.Name{Organization: []string{"pi-camera-go"}}, 81 | DNSNames: []string{"localhost"}, 82 | } 83 | 84 | println("Generating RSA key...") 85 | 86 | key, err := rsa.GenerateKey(rand.Reader, 2048) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | println("Generating certificate...") 92 | 93 | b, err := x509.CreateCertificate(rand.Reader, &cert, &cert, &key.PublicKey, key) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | privateKeyFile, err := os.Create(privateKeyPath) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | defer privateKeyFile.Close() 104 | block := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} 105 | if err := pem.Encode(privateKeyFile, &block); err != nil { 106 | return err 107 | } 108 | 109 | publicKeyFile, err := os.Create(publicKeyPath) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | defer publicKeyFile.Close() 115 | block = pem.Block{Type: "CERTIFICATE", Bytes: b} 116 | if err := pem.Encode(publicKeyFile, &block); err != nil { 117 | return err 118 | } 119 | 120 | println("Done generating certificate") 121 | return nil 122 | } -------------------------------------------------------------------------------- /server/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Josh A. Beam 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | * WHETHER IN CONTACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 22 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package util 27 | 28 | import ( 29 | "os" 30 | "os/user" 31 | "path" 32 | ) 33 | 34 | func ConfigDir(components ...string) (string, error) { 35 | u, err := user.Current() 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | configDir := path.Join(append([]string{u.HomeDir, ".pi-camera-go"}, components...)...) 41 | if err := os.MkdirAll(configDir, os.ModeDir|os.ModePerm); err != nil { 42 | return "", err 43 | } 44 | 45 | return configDir, nil 46 | } 47 | --------------------------------------------------------------------------------