├── .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 |
--------------------------------------------------------------------------------