├── mjpg_demo.gif ├── .gitignore ├── demo ├── demo.go ├── demo.html ├── multiple-mjpegstreams.html └── multiple-mjpegstreams.go ├── LICENSE ├── README.md ├── mjpegproxy.go └── mjpegproxy_test.go /mjpg_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/putsi/paparazzogo/HEAD/mjpg_demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /demo/demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/putsi/paparazzogo" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | // Local server settings 13 | imgPath := "/img.jpg" 14 | addr := ":8080" 15 | 16 | // MJPEG-stream settings 17 | user := "" 18 | pass := "" 19 | // If there is zero GET-requests for 30 seconds, mjpeg-stream will be closed. 20 | // Streaming will be reopened after next request. 21 | timeout := 30 * time.Second 22 | mjpegStream := "http://85.157.217.67/axis-cgi/mjpg/video.cgi" 23 | 24 | mjpegHandler := paparazzogo.NewMjpegproxy() 25 | mjpegHandler.OpenStream(mjpegStream, user, pass, timeout) 26 | 27 | http.Handle(imgPath, mjpegHandler) 28 | 29 | s := &http.Server{ 30 | Addr: addr, 31 | Handler: mjpegHandler, 32 | // Read- & Write-timeout prevent server from getting overwhelmed in idle connections 33 | ReadTimeout: 10 * time.Second, 34 | WriteTimeout: 10 * time.Second, 35 | } 36 | 37 | log.Fatal(s.ListenAndServe()) 38 | 39 | block := make(chan bool) 40 | // time.Sleep(time.Second * 30) 41 | // mp.CloseStream() 42 | // mp.OpenStream(newMjpegstream, newUser, newPass, newTimeout) 43 | <-block 44 | 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rodolfo Wilhelmy 4 | Copyright (c) 2014 Jarmo Puttonen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Paparazzo.go Demo 7 | 8 | 24 | 25 | 26 | 27 |
28 |

Paparazzo.go - Demo stream

29 |
30 |
31 | Loading image! 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /demo/multiple-mjpegstreams.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Paparazzo.go Demo 7 | 8 | 25 | 26 | 27 | 28 |
29 |

Paparazzo.go - Demo of multiple mjpeg-streams

30 |
31 |
32 | Loading image 1! 33 | Loading image 2! 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/multiple-mjpegstreams.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/putsi/paparazzogo" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | // Local server settings 13 | imgPath1 := "/img1.jpg" 14 | imgPath2 := "/img2.jpg" 15 | addr := ":8080" 16 | 17 | // MJPEG-stream settings 18 | user := "" 19 | pass := "" 20 | // If there is zero GET-requests for 30 seconds, mjpeg-stream will be closed. 21 | // Streaming will be reopened after next request. 22 | timeout := 30 * time.Second 23 | mjpegStream1 := "http://webcam.st-malo.com/axis-cgi/mjpg/video.cgi" 24 | mjpegStream2 := "http://85.157.217.67/axis-cgi/mjpg/video.cgi" 25 | 26 | mjpegHandler1 := paparazzogo.NewMjpegproxy() 27 | mjpegHandler1.OpenStream(mjpegStream1, user, pass, timeout) 28 | 29 | mjpegHandler2 := paparazzogo.NewMjpegproxy() 30 | mjpegHandler2.OpenStream(mjpegStream2, user, pass, timeout) 31 | 32 | mux := http.NewServeMux() 33 | mux.Handle(imgPath1, mjpegHandler1) 34 | mux.Handle(imgPath2, mjpegHandler2) 35 | 36 | s := &http.Server{ 37 | Addr: addr, 38 | Handler: mux, 39 | // Read- & Write-timeout prevent server from getting overwhelmed in idle connections 40 | ReadTimeout: 10 * time.Second, 41 | WriteTimeout: 10 * time.Second, 42 | } 43 | 44 | log.Fatal(s.ListenAndServe()) 45 | 46 | block := make(chan bool) 47 | // time.Sleep(time.Second * 30) 48 | // mjpegHandler2.CloseStream() 49 | // mjpegHandler2.OpenStream(newMjpegstream, newUser, newPass, newTimeout) 50 | <-block 51 | 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Paparazzo.go 2 | - 3 | 4 | _A stalker of [IP cameras](http://en.wikipedia.org/wiki/IP_camera)_ 5 | 6 | [![endorse](http://api.coderwall.com/putsi/endorsecount.png)](http://coderwall.com/putsi) 7 | 8 | **What is this?** 9 | 10 | A high performance caching web proxy for serving [MJPG](http://en.wikipedia.org/wiki/Motion_JPEG) streams to the masses. 11 | 12 | ***Features*** 13 | 14 | - Easy to use. 15 | - Done with [Go programming language](http://golang.org/). 16 | - Compatible with [http.HandlerFunc](http://golang.org/pkg/net/http/#HandlerFunc). 17 | - No unnecessary network traffic to IP-camera. 18 | - Serves MJPEG-stream as single jpeg-images. 19 | - Works with every browser. 20 | - Supports client-side caching. 21 | 22 | IPCamera (1) <-> (1) Paparazzo.go (1) <-> (N) Users 23 | 24 | ![Demo screenshot](https://raw.githubusercontent.com/putsi/paparazzogo/master/mjpg_demo.gif "Streaming a VIVOTEK camera") 25 | 26 | Background 27 | - 28 | 29 | **IP cameras can't handle web traffic** 30 | 31 | IP cameras are slow devices that can't handle a regular amount of web traffic. So if you plan to go public with an IP camera you have the following options: 32 | 33 | 1. **The naive approach** - Embed the camera service directly in your site, e.g. http://201.166.63.44/axis-cgi/jpg/image.cgi?resolution=CIF. 34 | 2. **Ye olde approach** - Serve images as static files in your server. I've found that several sites use this approach through messy PHP background jobs that update this files at slow intervals, generating excessive (and unnecessary) disk accesses. 35 | 3. **Plug n' pray approach** - Embed a flash or Java-based player, such as the [Cambozola](http://www.charliemouse.com/code/cambozola/) player. This requires plugins. 36 | 4. **MJPG proxy** - Serve the MJPG stream directly if you are targeting only grade A browsers, (sorry IE). 37 | 5. **Paparazzo.go: A web service of dynamic images** - Build a MJPG proxy server which parses the stream, updates images in memory, and delivers new images on demand. This approach is scalable, elegant, blazing fast and doesn't require disk access. 38 | 39 | Usage 40 | - 41 | 42 | Get Paparazzo and start demo: 43 | ``` 44 | go get github.com/putsi/paparazzogo 45 | 46 | cd $GOPATH/src/github.com/putsi/paparazzogo/demo 47 | go run demo.go 48 | open demo.html 49 | ``` 50 | 51 | Customization of settings: 52 | ``` 53 | mjpegHandler := &paparazzogo.Mjpegproxy{ 54 | // Max MJPEG-frame size (5Mb by default). 55 | partbufsize: 625000, 56 | 57 | // Sleep time between error and reconnecting to stream (one second by default). 58 | waittime: time.Second * 1, 59 | 60 | // How long to use one stream response before reconnecting (one hour by default). 61 | responseduration: time.Hour, 62 | 63 | // Caching enables/disables support for client-side caching 64 | // of jpg-files. If enabled, saves bandwidth. 65 | // If disabled, allows more than one frame per second. 66 | // Enabled by default. 67 | caching: true, 68 | } 69 | ``` 70 | 71 | **See more examples in demo-folder.** 72 | 73 | Licence 74 | - 75 | Use of this source code is governed by a MIT-style licence that can be found in the [LICENCE](https://raw.githubusercontent.com/putsi/paparazzogo/master/LICENSE)-file. 76 | 77 | See Also 78 | - 79 | **[The original Paparazzo.js for NodeJS!](https://github.com/rodowi/Paparazzo.js)** -------------------------------------------------------------------------------- /mjpegproxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Jarmo Puttonen . All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // licence that can be found in the LICENCE file. 4 | 5 | /*Package paparazzogo implements a caching proxy for 6 | serving MJPEG-stream as JPG-images. 7 | */ 8 | package paparazzogo 9 | 10 | import ( 11 | "bytes" 12 | "errors" 13 | "io" 14 | "log" 15 | "mime" 16 | "mime/multipart" 17 | "net" 18 | "net/http" 19 | "strings" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | // A Mjpegproxy implements http.Handler interface and generates 25 | // JPG-images from a MJPEG-stream. 26 | type Mjpegproxy struct { 27 | partbufsize int64 28 | waittime time.Duration 29 | responseduration time.Duration 30 | caching bool 31 | 32 | mjpegStream string 33 | curImg bytes.Buffer 34 | curImgLock sync.RWMutex 35 | conChan chan time.Time 36 | lastConn time.Time 37 | lastConnLock sync.RWMutex 38 | lastModified time.Time 39 | lastModLock sync.RWMutex 40 | running bool 41 | runningLock sync.RWMutex 42 | l net.Listener 43 | writer io.Writer 44 | handler http.Handler 45 | } 46 | 47 | // NewMjpegproxy returns a new Mjpegproxy with default values. 48 | func NewMjpegproxy() *Mjpegproxy { 49 | p := &Mjpegproxy{ 50 | // Max MJPEG-frame size 5Mb. 51 | partbufsize: 625000, 52 | // Sleep time between error and reconnecting to stream. 53 | waittime: time.Second * 1, 54 | // How long to use one stream response before reconnecting. 55 | responseduration: time.Hour, 56 | // Caching enables/disables support for client-side caching 57 | // of jpg-files. If enabled, saves bandwidth. 58 | // If disabled, allows more than one frame per second. 59 | caching: false, 60 | } 61 | return p 62 | } 63 | 64 | // ServeHTTP uses w to serve current last MJPEG-frame 65 | // as JPG. It also reopens MJPEG-stream 66 | // if it was closed by idle timeout. 67 | func (m *Mjpegproxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { 68 | select { 69 | case m.conChan <- time.Now(): 70 | default: 71 | m.lastConnLock.Lock() 72 | m.lastConn = time.Now() 73 | m.lastConnLock.Unlock() 74 | } 75 | buf := bytes.Buffer{} 76 | m.curImgLock.RLock() 77 | buf.Write(m.curImg.Bytes()) 78 | m.curImgLock.RUnlock() 79 | 80 | reader := bytes.NewReader(buf.Bytes()) 81 | if reader == nil { 82 | log.Println(m.mjpegStream, "ServeHTTP could not create bytes.Reader!") 83 | return 84 | } 85 | if !m.caching { 86 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 87 | w.Header().Set("Pragma", "no-cache") 88 | w.Header().Set("Expires", "0") 89 | w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 90 | reader.WriteTo(w) 91 | } else { 92 | m.lastModLock.RLock() 93 | modtime := m.lastModified 94 | m.lastModLock.RUnlock() 95 | if modtime.String() == "" { 96 | modtime = time.Now() 97 | } 98 | http.ServeContent(w, req, "img.jpg", modtime, reader) 99 | } 100 | } 101 | 102 | // CloseStream stops and closes MJPEG-stream. 103 | func (m *Mjpegproxy) CloseStream() { 104 | m.setRunning(false) 105 | } 106 | 107 | // OpenStream creates a go-routine of openstream. 108 | func (m *Mjpegproxy) OpenStream(mjpegStream, user, pass string, timeout time.Duration) { 109 | go m.openstream(mjpegStream, user, pass, timeout) 110 | } 111 | 112 | // GetRunning returns state of openstream. 113 | func (m *Mjpegproxy) GetRunning() bool { 114 | m.runningLock.RLock() 115 | defer m.runningLock.RUnlock() 116 | return m.running 117 | } 118 | 119 | func (m *Mjpegproxy) setRunning(r bool) { 120 | m.runningLock.Lock() 121 | defer m.runningLock.Unlock() 122 | m.running = r 123 | } 124 | 125 | func (m *Mjpegproxy) getresponse(request *http.Request) (*http.Response, error) { 126 | tr := &http.Transport{DisableKeepAlives: true} 127 | client := &http.Client{Transport: tr} 128 | response, err := client.Do(request) 129 | if err != nil { 130 | return nil, err 131 | } 132 | if response.StatusCode != 200 { 133 | response.Body.Close() 134 | errs := "Got invalid response status: " + response.Status 135 | return nil, errors.New(errs) 136 | } 137 | return response, nil 138 | } 139 | 140 | func (m *Mjpegproxy) getboundary(response *http.Response) (string, error) { 141 | header := response.Header.Get("Content-Type") 142 | if header == "" { 143 | return "", errors.New("Content-Type isn't specified!") 144 | } 145 | ct, params, err := mime.ParseMediaType(header) 146 | if err != nil { 147 | return "", err 148 | } 149 | if ct != "multipart/x-mixed-replace" { 150 | errs := "Wrong Content-Type: expected multipart/x-mixed-replace, got " + ct 151 | return "", errors.New(errs) 152 | } 153 | boundary, ok := params["boundary"] 154 | if !ok { 155 | return "", errors.New("No multipart boundary param in Content-Type!") 156 | } 157 | // Some IP-cameras screw up boundary strings so we 158 | // have to remove excessive "--" characters manually. 159 | boundary = strings.Replace(boundary, "--", "", -1) 160 | return boundary, nil 161 | } 162 | 163 | // OpenStream sends request to target and handles 164 | // response. It opens MJPEG-stream and copies received 165 | // frame to m.curImg. It closes stream if m.CloseStream() 166 | // is called or if difference between current time and 167 | // time of last request to ServeHTTP is bigger than timeout. 168 | func (m *Mjpegproxy) openstream(mjpegStream, user, pass string, timeout time.Duration) { 169 | m.setRunning(true) 170 | m.conChan = make(chan time.Time) 171 | m.mjpegStream = mjpegStream 172 | var lastconn time.Time 173 | var img *multipart.Part 174 | 175 | request, err := http.NewRequest("GET", mjpegStream, nil) 176 | if err != nil { 177 | log.Fatal(m.mjpegStream, err) 178 | } 179 | if user != "" && pass != "" { 180 | request.SetBasicAuth(user, pass) 181 | } 182 | var response *http.Response 183 | var boundary string 184 | var mpread *multipart.Reader 185 | var starttime time.Time 186 | buf := new(bytes.Buffer) 187 | 188 | log.Println("Starting streaming from", mjpegStream) 189 | 190 | for m.GetRunning() { 191 | lastconn = <-m.conChan 192 | m.lastConnLock.Lock() 193 | m.lastConn = lastconn 194 | m.lastConnLock.Unlock() 195 | if !m.GetRunning() { 196 | continue 197 | } 198 | 199 | response, err = m.getresponse(request) 200 | if err != nil { 201 | log.Println(m.mjpegStream, err) 202 | time.Sleep(m.waittime) 203 | continue 204 | } 205 | starttime = time.Now() 206 | boundary, err = m.getboundary(response) 207 | 208 | if err != nil { 209 | log.Println(m.mjpegStream, err) 210 | response.Body.Close() 211 | time.Sleep(m.waittime) 212 | continue 213 | } 214 | mpread = multipart.NewReader(response.Body, boundary) 215 | for m.GetRunning() && (time.Since(lastconn) < timeout) && err == nil { 216 | if time.Since(starttime) > m.responseduration { 217 | break 218 | } 219 | if time.Since(lastconn) > timeout/2 { 220 | m.lastConnLock.RLock() 221 | lastconn = m.lastConn 222 | m.lastConnLock.RUnlock() 223 | } 224 | img, err = mpread.NextPart() 225 | if err != nil { 226 | log.Println(m.mjpegStream, err) 227 | break 228 | } 229 | // buf is an additional buffer that allows 230 | // serving curImg while loading next part. 231 | buf.Reset() 232 | _, err = buf.ReadFrom(io.LimitReader(img, m.partbufsize)) 233 | if err != nil { 234 | img.Close() 235 | log.Println(m.mjpegStream, err) 236 | break 237 | } 238 | if m.caching { 239 | m.lastModLock.Lock() 240 | m.lastModified = time.Now().UTC() 241 | m.lastModLock.Unlock() 242 | } 243 | m.curImgLock.Lock() 244 | m.curImg.Reset() 245 | _, err = m.curImg.ReadFrom(buf) 246 | m.curImgLock.Unlock() 247 | img.Close() 248 | if err != nil { 249 | log.Println(m.mjpegStream, err) 250 | break 251 | } 252 | } 253 | response.Body.Close() 254 | time.Sleep(m.waittime) 255 | } 256 | log.Println("Stopped streaming from", mjpegStream) 257 | } 258 | -------------------------------------------------------------------------------- /mjpegproxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Jarmo Puttonen . All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // licence that can be found in the LICENCE file. 4 | 5 | /*Package paparazzogo implements a caching proxy for 6 | serving MJPEG-stream as JPG-images. 7 | */ 8 | package paparazzogo 9 | 10 | /* 11 | Test coverage: 86.9% of statements 12 | */ 13 | 14 | import ( 15 | "bytes" 16 | "fmt" 17 | "io/ioutil" 18 | "net/http" 19 | "net/http/httptest" 20 | "strconv" 21 | "strings" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | //Multipart body for testing. 27 | var firstPart = "01234567890" 28 | var boundary = "MyBoundary" 29 | var validBody = ` 30 | Content-type: multipart/x-mixed-replace;boundary=` + boundary + ` 31 | 32 | --` + boundary + ` 33 | Content-type: text/plain 34 | 35 | ` + firstPart + ` 36 | 37 | --` + boundary + `-- 38 | ` 39 | var invalidBody = ` 40 | Content-type: multipart/x-mixed-replace;boundary=` + boundary + ` 41 | --` + boundary + ` 42 | Content-type: text/plain 43 | ` + firstPart + ` 44 | --` + boundary + `-- 45 | ` 46 | var malformedPart = ` 47 | Content-type: multipart/x-mixed-replace;boundary=` + boundary + ` 48 | 49 | --` + boundary + ` 50 | Content-type: text/plain 51 | 52 | ` + firstPart + ` 53 | 54 | --` + boundary + ` 55 | ` 56 | 57 | var streamBody = ` 58 | --` + boundary + ` 59 | Content-type: text/plain 60 | 61 | ` + firstPart + ` 62 | 63 | --` + boundary + `-- 64 | ` 65 | 66 | func Test_NewMjpegproxy(t *testing.T) { 67 | mp := NewMjpegproxy() 68 | if mp == nil { 69 | t.Fatal("Could not create Mjpegproxy!") 70 | } 71 | } 72 | 73 | func Test_CloseStream(t *testing.T) { 74 | mp := NewMjpegproxy() 75 | mp.CloseStream() 76 | if mp.running != false { 77 | t.Fatalf("Wrong run state: %s", mp.running) 78 | } 79 | } 80 | 81 | func Test_setRunning(t *testing.T) { 82 | mp := NewMjpegproxy() 83 | mp.setRunning(true) 84 | if mp.running != true { 85 | t.Fatalf("Wrong run state: expected true, got %s", mp.running) 86 | } 87 | mp.setRunning(false) 88 | if mp.running != false { 89 | t.Fatalf("Wrong run state: expected false, got %s", mp.running) 90 | } 91 | } 92 | 93 | func Test_GetRunning(t *testing.T) { 94 | mp := NewMjpegproxy() 95 | mp.setRunning(true) 96 | if mp.GetRunning() != true { 97 | t.Fatalf("Wrong run state: expected true, got %s", mp.GetRunning()) 98 | } 99 | mp.setRunning(false) 100 | if mp.running != false { 101 | t.Fatalf("Wrong run state: expected false, got %s", mp.GetRunning()) 102 | } 103 | } 104 | 105 | func Test_getresponse_valid(t *testing.T) { 106 | msg := "Test string" 107 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 108 | fmt.Fprintln(w, msg) 109 | })) 110 | defer ts.Close() 111 | mp := NewMjpegproxy() 112 | request, err := http.NewRequest("GET", ts.URL, nil) 113 | if err != nil { 114 | t.Fatal("Failed to create request.") 115 | } 116 | res, err := mp.getresponse(request) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | responsebody, err := ioutil.ReadAll(res.Body) 121 | res.Body.Close() 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | if !strings.Contains(string(responsebody), msg) { 126 | t.Fatalf("Response body mismatch: %s vs %s", msg, string(responsebody)) 127 | } 128 | } 129 | 130 | func Test_getresponse_invalid_status(t *testing.T) { 131 | invalidstatus := 418 132 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 133 | http.Error(w, "", invalidstatus) 134 | })) 135 | defer ts.Close() 136 | mp := NewMjpegproxy() 137 | request, err := http.NewRequest("GET", ts.URL, nil) 138 | if err != nil { 139 | t.Fatal("Failed to create request.") 140 | } 141 | _, err = mp.getresponse(request) 142 | if err == nil { 143 | t.Fatal("Unexpected nil error!") 144 | } 145 | str := strconv.Itoa(invalidstatus) 146 | if !strings.Contains(err.Error(), str) { 147 | t.Fatalf("Wrong status code: %s vs %s", err.Error(), str) 148 | } 149 | } 150 | func Test_getresponse_noconnection(t *testing.T) { 151 | mp := NewMjpegproxy() 152 | request, err := http.NewRequest("GET", "http://127.0.0.1:99999/", nil) 153 | if err != nil { 154 | t.Fatal("Failed to create request.") 155 | } 156 | _, err = mp.getresponse(request) 157 | if err == nil { 158 | t.Fatal("Unexpected nil error!") 159 | } 160 | if !strings.Contains(err.Error(), "invalid port 99999") { 161 | t.Fatalf("Wrong error on connection refuse: %s", err.Error()) 162 | } 163 | } 164 | 165 | func Test_getboundary_valid(t *testing.T) { 166 | mp := NewMjpegproxy() 167 | response := &http.Response{ 168 | Status: "200 OK", 169 | StatusCode: 200, 170 | Proto: "HTTP/1.0", 171 | Header: http.Header{ 172 | "Content-Type": []string{"multipart/x-mixed-replace; boundary=" + boundary}, 173 | }, 174 | Body: ioutil.NopCloser(bytes.NewBufferString(validBody)), 175 | ContentLength: int64(len(validBody)), 176 | } 177 | _, err := mp.getboundary(response) 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | } 182 | 183 | func Test_getboundary_noboundary(t *testing.T) { 184 | mp := NewMjpegproxy() 185 | response := &http.Response{ 186 | Status: "200 OK", 187 | StatusCode: 200, 188 | Proto: "HTTP/1.0", 189 | Header: http.Header{ 190 | "Content-Type": []string{"multipart/x-mixed-replace"}, 191 | }, 192 | Body: ioutil.NopCloser(bytes.NewBufferString(validBody)), 193 | ContentLength: int64(len(validBody)), 194 | } 195 | _, err := mp.getboundary(response) 196 | if err == nil { 197 | t.Fatal("Unexpected nil error!") 198 | } 199 | if !strings.Contains(err.Error(), "No multipart boundary param in Content-Type!") { 200 | t.Fatalf("Wrong error: %s", err.Error()) 201 | } 202 | } 203 | 204 | func Test_getboundary_wrong(t *testing.T) { 205 | mp := NewMjpegproxy() 206 | response := &http.Response{ 207 | Status: "200 OK", 208 | StatusCode: 200, 209 | Proto: "HTTP/1.0", 210 | Header: http.Header{ 211 | "Content-Type": []string{"multipart/form-data"}, 212 | }, 213 | Body: ioutil.NopCloser(bytes.NewBufferString(validBody)), 214 | ContentLength: int64(len(validBody)), 215 | } 216 | _, err := mp.getboundary(response) 217 | if err == nil { 218 | t.Fatal("Unexpected nil error!") 219 | } 220 | if !strings.Contains(err.Error(), "Wrong Content-Type: expected multipart/x-mixed-replace, got multipart/form-data") { 221 | t.Fatalf("Wrong error: %s", err.Error()) 222 | } 223 | 224 | } 225 | 226 | func Test_getboundary_invalid(t *testing.T) { 227 | mp := NewMjpegproxy() 228 | response := &http.Response{ 229 | Status: "200 OK", 230 | StatusCode: 200, 231 | Proto: "HTTP/1.0", 232 | Header: http.Header{ 233 | "Content-Type": []string{"multipart/"}, 234 | }, 235 | Body: ioutil.NopCloser(bytes.NewBufferString(validBody)), 236 | ContentLength: int64(len(validBody)), 237 | } 238 | _, err := mp.getboundary(response) 239 | if err == nil { 240 | t.Fatal("Unexpected nil error!") 241 | } 242 | if !strings.Contains(err.Error(), "mime: expected token after slash") { 243 | t.Fatalf("Wrong error: %s", err.Error()) 244 | } 245 | 246 | } 247 | 248 | func Test_getboundary_noCT(t *testing.T) { 249 | mp := NewMjpegproxy() 250 | response := &http.Response{ 251 | Status: "200 OK", 252 | StatusCode: 200, 253 | Proto: "HTTP/1.0", 254 | Header: http.Header{}, 255 | Body: ioutil.NopCloser(bytes.NewBufferString(validBody)), 256 | ContentLength: int64(len(validBody)), 257 | } 258 | _, err := mp.getboundary(response) 259 | if err == nil { 260 | t.Fatal("Unexpected nil error!") 261 | } 262 | if !strings.Contains(err.Error(), "Content-Type isn't specified!") { 263 | t.Fatalf("Wrong error: %s", err.Error()) 264 | } 265 | 266 | } 267 | 268 | func Test_ServeHTTP(t *testing.T) { 269 | mp := NewMjpegproxy() 270 | req := &http.Request{} 271 | testString := []byte("Test String") 272 | mp.curImg.Write(testString) 273 | recorder := httptest.NewRecorder() 274 | mp.ServeHTTP(recorder, req) 275 | if time.Since(mp.lastConn) > time.Second { 276 | t.Fatal("Unexpected lastconn value!") 277 | } 278 | if !bytes.Equal(recorder.Body.Bytes(), testString) { 279 | t.Fatalf("Content mismatch: expected %s, got %s", string(testString), string(mp.curImg.Bytes())) 280 | } 281 | } 282 | 283 | func Test_OpenStream_logic(t *testing.T) { 284 | user := "user" 285 | pass := "pass" 286 | mp := NewMjpegproxy() 287 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 288 | w.Header().Set("Content-Type", "multipart/x-mixed-replace;boundary="+boundary) 289 | fmt.Fprintln(w, streamBody) 290 | })) 291 | defer ts.Close() 292 | mp.setRunning(true) 293 | mp.OpenStream(ts.URL, user, pass, time.Second) 294 | defer mp.CloseStream() 295 | time.Sleep(time.Second) 296 | mp.conChan <- time.Now() 297 | time.Sleep(time.Millisecond*50) 298 | if !strings.Contains(mp.curImg.String(), firstPart) { 299 | t.Fatalf("Wrong response: expected %s, got %s", firstPart, mp.curImg.String()) 300 | } 301 | mp.setRunning(false) 302 | } 303 | --------------------------------------------------------------------------------