├── .gitignore ├── LICENSE ├── README.md ├── _example ├── camera │ ├── go.mod │ ├── go.sum │ ├── haarcascade_frontalface_default.xml │ └── main.go └── mjpegproxy │ └── mjpegproxy.go ├── go.mod └── mjpeg.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mattn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mjpeg 2 | 3 | Motion Jpeg Encoder/Decoder 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/mattn/go-mjpeg 9 | ``` 10 | 11 | ## License 12 | 13 | MIT 14 | 15 | ## Author 16 | 17 | Yasuhiro Matsumoto (a.k.a. mattn) 18 | -------------------------------------------------------------------------------- /_example/camera/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-mjpeg/_example/camera 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/mattn/go-mjpeg v0.0.2 // indirect 7 | gocv.io/x/gocv v0.28.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /_example/camera/go.sum: -------------------------------------------------------------------------------- 1 | github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs= 2 | github.com/mattn/go-mjpeg v0.0.1 h1:PunfTzQvXkULfur7ALrU8EeMWM4kknyJT7zI1DUoNIQ= 3 | github.com/mattn/go-mjpeg v0.0.1/go.mod h1:m4++VtXCgYJxuZeEUF3WydpJk+0yKO50el52Ww5xBvo= 4 | github.com/mattn/go-mjpeg v0.0.2 h1:AtIjibezL+DO8XT5x4tMLdzSmhvAEbSOKGol87UZN7k= 5 | github.com/mattn/go-mjpeg v0.0.2/go.mod h1:65z7Cj+u5y5K3B8Sy5NtrJFTWAhguGHs9FEkADdx6kE= 6 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 7 | gocv.io/x/gocv v0.24.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= 8 | gocv.io/x/gocv v0.28.0 h1:hweRS9Js60YEZPZzjhU5I+0E2ngazquLlO78zwnrFvY= 9 | gocv.io/x/gocv v0.28.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= 10 | -------------------------------------------------------------------------------- /_example/camera/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "image/color" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/mattn/go-mjpeg" 16 | 17 | "gocv.io/x/gocv" 18 | ) 19 | 20 | var ( 21 | camera = flag.String("camera", "0", "Camera ID") 22 | addr = flag.String("addr", ":8080", "Server address") 23 | xml = flag.String("classifier", "haarcascade_frontalface_default.xml", "classifier XML file") 24 | interval = flag.Duration("interval", 200*time.Millisecond, "interval") 25 | ) 26 | 27 | func capture(ctx context.Context, wg *sync.WaitGroup, stream *mjpeg.Stream) { 28 | defer wg.Done() 29 | 30 | var webcam *gocv.VideoCapture 31 | var err error 32 | if id, err := strconv.ParseInt(*camera, 10, 64); err == nil { 33 | webcam, err = gocv.VideoCaptureDevice(int(id)) 34 | } else { 35 | webcam, err = gocv.VideoCaptureFile(*camera) 36 | } 37 | if err != nil { 38 | log.Println("unable to init web cam: %v", err) 39 | return 40 | } 41 | defer webcam.Close() 42 | 43 | classifier := gocv.NewCascadeClassifier() 44 | defer classifier.Close() 45 | if !classifier.Load(*xml) { 46 | log.Println("unable to load:", *xml) 47 | return 48 | } 49 | 50 | im := gocv.NewMat() 51 | 52 | for len(ctx.Done()) == 0 { 53 | var buf []byte 54 | if stream.NWatch() > 0 { 55 | if ok := webcam.Read(&im); !ok { 56 | continue 57 | } 58 | 59 | rects := classifier.DetectMultiScale(im) 60 | for _, r := range rects { 61 | face := im.Region(r) 62 | face.Close() 63 | gocv.Rectangle(&im, r, color.RGBA{0, 0, 255, 0}, 2) 64 | } 65 | nbuf, err := gocv.IMEncode(".jpg", im) 66 | if err != nil { 67 | continue 68 | } 69 | buf = nbuf.GetBytes() 70 | } 71 | err = stream.Update(buf) 72 | if err != nil { 73 | break 74 | } 75 | } 76 | } 77 | 78 | func main() { 79 | flag.Parse() 80 | 81 | stream := mjpeg.NewStreamWithInterval(*interval) 82 | 83 | ctx, cancel := context.WithCancel(context.Background()) 84 | var wg sync.WaitGroup 85 | wg.Add(1) 86 | go capture(ctx, &wg, stream) 87 | 88 | http.HandleFunc("/mjpeg", stream.ServeHTTP) 89 | 90 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 91 | w.Header().Set("Content-Type", "text/html") 92 | w.Write([]byte(``)) 93 | }) 94 | 95 | server := &http.Server{Addr: *addr} 96 | sc := make(chan os.Signal, 1) 97 | signal.Notify(sc, os.Interrupt) 98 | go func() { 99 | <-sc 100 | server.Shutdown(ctx) 101 | }() 102 | server.ListenAndServe() 103 | stream.Close() 104 | cancel() 105 | 106 | wg.Wait() 107 | } 108 | -------------------------------------------------------------------------------- /_example/mjpegproxy/mjpegproxy.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "time" 15 | 16 | "github.com/mattn/go-mjpeg" 17 | ) 18 | 19 | var ( 20 | url = flag.String("url", "", "Camera host") 21 | addr = flag.String("addr", ":8080", "Server address") 22 | interval = flag.Duration("interval", 200*time.Millisecond, "interval") 23 | ) 24 | 25 | func proxy(wg *sync.WaitGroup, stream *mjpeg.Stream) { 26 | defer wg.Done() 27 | 28 | dec, err := mjpeg.NewDecoderFromURL(*url) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | for { 34 | b, err := dec.DecodeRaw() 35 | if err != nil { 36 | break 37 | } 38 | err = stream.Update(b) 39 | if err != nil { 40 | break 41 | } 42 | } 43 | } 44 | 45 | func main() { 46 | flag.Parse() 47 | if *url == "" { 48 | flag.Usage() 49 | os.Exit(1) 50 | } 51 | 52 | stream := mjpeg.NewStreamWithInterval(*interval) 53 | 54 | var wg sync.WaitGroup 55 | wg.Add(1) 56 | go proxy(&wg, stream) 57 | 58 | http.HandleFunc("/jpeg", func(w http.ResponseWriter, r *http.Request) { 59 | w.Header().Set("Content-Type", "image/jpeg") 60 | w.Write(stream.Current()) 61 | }) 62 | 63 | http.HandleFunc("/mjpeg", stream.ServeHTTP) 64 | 65 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 66 | w.Header().Set("Content-Type", "text/html") 67 | w.Write([]byte(``)) 68 | }) 69 | 70 | server := &http.Server{Addr: *addr} 71 | sc := make(chan os.Signal, 1) 72 | signal.Notify(sc, os.Interrupt) 73 | go func() { 74 | <-sc 75 | server.Shutdown(context.Background()) 76 | }() 77 | server.ListenAndServe() 78 | stream.Close() 79 | 80 | wg.Wait() 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-mjpeg 2 | 3 | go 1.16 -------------------------------------------------------------------------------- /mjpeg.go: -------------------------------------------------------------------------------- 1 | package mjpeg 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "io" 10 | "mime" 11 | "mime/multipart" 12 | "net/http" 13 | "net/textproto" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // Decoder decode motion jpeg 20 | type Decoder struct { 21 | r *multipart.Reader 22 | m sync.Mutex 23 | } 24 | 25 | // NewDecoder return new instance of Decoder 26 | func NewDecoder(r io.Reader, b string) *Decoder { 27 | d := new(Decoder) 28 | d.r = multipart.NewReader(r, b) 29 | return d 30 | } 31 | 32 | // NewDecoderFromResponse return new instance of Decoder from http.Response 33 | func NewDecoderFromResponse(res *http.Response) (*Decoder, error) { 34 | _, param, err := mime.ParseMediaType(res.Header.Get("Content-Type")) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return NewDecoder(res.Body, strings.Trim(param["boundary"], "-")), nil 39 | } 40 | 41 | // NewDecoderFromURL return new instance of Decoder from response which specified URL 42 | func NewDecoderFromURL(u string) (*Decoder, error) { 43 | req, err := http.NewRequest("GET", u, nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | res, err := http.DefaultClient.Do(req) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return NewDecoderFromResponse(res) 52 | } 53 | 54 | // Decode do decoding 55 | func (d *Decoder) Decode() (image.Image, error) { 56 | p, err := d.r.NextPart() 57 | if err != nil { 58 | return nil, err 59 | } 60 | return jpeg.Decode(p) 61 | } 62 | 63 | // DecodeRaw do decoding raw bytes 64 | func (d *Decoder) DecodeRaw() ([]byte, error) { 65 | p, err := d.r.NextPart() 66 | if err != nil { 67 | return nil, err 68 | } 69 | var buf bytes.Buffer 70 | _, err = io.Copy(&buf, p) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return buf.Bytes(), nil 75 | } 76 | 77 | type Stream struct { 78 | m sync.Mutex 79 | s map[chan []byte]struct{} 80 | Interval time.Duration 81 | } 82 | 83 | func NewStream() *Stream { 84 | return &Stream{ 85 | s: make(map[chan []byte]struct{}), 86 | } 87 | } 88 | 89 | func NewStreamWithInterval(interval time.Duration) *Stream { 90 | return &Stream{ 91 | s: make(map[chan []byte]struct{}), 92 | Interval: interval, 93 | } 94 | } 95 | 96 | func (s *Stream) Closed() bool { 97 | s.m.Lock() 98 | defer s.m.Unlock() 99 | return s.s == nil 100 | } 101 | 102 | func (s *Stream) Close() error { 103 | s.m.Lock() 104 | defer s.m.Unlock() 105 | for c := range s.s { 106 | close(c) 107 | delete(s.s, c) 108 | } 109 | s.s = nil 110 | return nil 111 | } 112 | 113 | func (s *Stream) Update(b []byte) error { 114 | s.m.Lock() 115 | defer s.m.Unlock() 116 | if s.s == nil { 117 | return errors.New("stream was closed") 118 | } 119 | for c := range s.s { 120 | select { 121 | case c <- b: 122 | default: 123 | } 124 | } 125 | return nil 126 | } 127 | 128 | func (s *Stream) add(c chan []byte) { 129 | s.m.Lock() 130 | s.s[c] = struct{}{} 131 | s.m.Unlock() 132 | } 133 | 134 | func (s *Stream) destroy(c chan []byte) { 135 | s.m.Lock() 136 | if s.s != nil { 137 | close(c) 138 | delete(s.s, c) 139 | } 140 | s.m.Unlock() 141 | } 142 | 143 | func (s *Stream) NWatch() int { 144 | return len(s.s) 145 | } 146 | 147 | func (s *Stream) Current() []byte { 148 | c := make(chan []byte) 149 | s.add(c) 150 | defer s.destroy(c) 151 | 152 | return <-c 153 | } 154 | 155 | func (s *Stream) ServeHTTP(w http.ResponseWriter, r *http.Request) { 156 | c := make(chan []byte) 157 | s.add(c) 158 | defer s.destroy(c) 159 | 160 | m := multipart.NewWriter(w) 161 | defer m.Close() 162 | 163 | w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary="+m.Boundary()) 164 | w.Header().Set("Connection", "close") 165 | h := textproto.MIMEHeader{} 166 | st := fmt.Sprint(time.Now().Unix()) 167 | for { 168 | time.Sleep(s.Interval) 169 | 170 | b, ok := <-c 171 | if !ok { 172 | break 173 | } 174 | h.Set("Content-Type", "image/jpeg") 175 | h.Set("Content-Length", fmt.Sprint(len(b))) 176 | h.Set("X-StartTime", st) 177 | h.Set("X-TimeStamp", fmt.Sprint(time.Now().Unix())) 178 | mw, err := m.CreatePart(h) 179 | if err != nil { 180 | break 181 | } 182 | _, err = mw.Write(b) 183 | if err != nil { 184 | break 185 | } 186 | if flusher, ok := mw.(http.Flusher); ok { 187 | flusher.Flush() 188 | } 189 | } 190 | } 191 | --------------------------------------------------------------------------------