├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── index.html ├── stats.min.js └── ws-relay.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 cs8425 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 | # ffmpeg ws-relay 2 | This is a simple example show up how to streaming video from ffmpeg by broadcasting image frames via websocket. 3 | 4 | 5 | ## Build 6 | 7 | 1. install go: [golang](https://golang.org/dl/) 8 | 2. clone this repo : `git clone https://github.com/cs8425/ffmpeg-ws-relay.git` 9 | 3. build: `go build -o ws-relay ws-relay.go` 10 | 4. run with ffmpeg, see [Usage example](#usage-example) 11 | 5. open browser to: [http://127.0.0.1:8080/](http://127.0.0.1:8080/) 12 | 13 | 14 | ## Usage example: 15 | 16 | * transcode a file to websocket via png format: 17 | * `ffmpeg -re -i v01.mp4 -c:v png -f image2pipe - | ./ws-relay -l :8080 -s png` 18 | * transcode a file to websocket via jpg format: 19 | * `ffmpeg -re -i v01.mp4 -s 1280x720 -c:v mjpeg -qscale:v 2 -f image2pipe - | ./ws-relay -l :8080` 20 | * `-s 1280x720` : output size 21 | * `-qscale:v 2` : jpeg quality, range 2~31, 31 is the worst quality 22 | 23 | 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cs8425/ffmpeg-ws-relay 2 | 3 | go 1.12 4 | 5 | require github.com/gorilla/websocket v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 2 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | broacast image stream via websocket 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 106 | 107 | -------------------------------------------------------------------------------- /stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){function h(a){c.appendChild(a.dom);return a}function k(a){for(var d=0;de+1E3&&(r.update(1E3*a/(c-e),100),e=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){g=this.end()},domElement:c,setMode:k}}; 4 | Stats.Panel=function(h,k,l){var c=Infinity,g=0,e=Math.round,a=e(window.devicePixelRatio||1),r=80*a,f=48*a,t=3*a,u=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=f;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,f);b.fillStyle=k;b.fillText(h,t,u);b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(f, 5 | v){c=Math.min(c,f);g=Math.max(g,f);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=k;b.fillText(e(f)+" "+h+" ("+e(c)+"-"+e(g)+")",t,u);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,e((1-f/v)*p))}}};"object"===typeof module&&(module.exports=Stats); 6 | -------------------------------------------------------------------------------- /ws-relay.go: -------------------------------------------------------------------------------- 1 | // for build: go build -o ws-relay ws-relay.go 2 | /* 3 | 4 | usage: 5 | 6 | ffmpeg -re -i v01.mp4 -c:v png -f image2pipe - | ./ws-relay -l :8081 -s png 7 | ffmpeg -re -i v01.mp4 -s 1280x720 -c:v mjpeg -qscale:v 2 -f image2pipe - | ./ws-relay -l :8081 8 | 9 | */ 10 | package main 11 | 12 | import ( 13 | "net/http" 14 | "log" 15 | "flag" 16 | "errors" 17 | 18 | "encoding/binary" 19 | "io" 20 | "bufio" 21 | "bytes" 22 | "os" 23 | 24 | ws "github.com/gorilla/websocket" 25 | ) 26 | 27 | var localAddr = flag.String("l", ":8080", "") 28 | 29 | var wsComp = flag.Bool("wscomp", false, "ws compression") 30 | var verbosity = flag.Int("v", 3, "verbosity") 31 | 32 | var queue = flag.Int("q", 1, "ws queue") 33 | 34 | var split = flag.String("s", "jpg", "image type") 35 | 36 | var upgrader = ws.Upgrader{ EnableCompression: false } // use default options 37 | 38 | var newclients chan *WsClient 39 | var bufCh chan []byte 40 | 41 | type WsClient struct { 42 | *ws.Conn 43 | data chan []byte 44 | die bool 45 | } 46 | func NewWsClient(c *ws.Conn) (*WsClient) { 47 | return &WsClient{ c, make(chan []byte, *queue), false } 48 | } 49 | func (c *WsClient) Send(buf []byte) (error) { 50 | if c.die { 51 | return errors.New("ws connection die") 52 | } 53 | 54 | select { 55 | case <- c.data: 56 | default: 57 | } 58 | c.data <- buf 59 | 60 | return nil 61 | } 62 | func (c *WsClient) worker() { 63 | for { 64 | buf := <- c.data 65 | //Vln(5, "[dbg]worker()", &c, len(buf)) 66 | err := c.WriteMessage(ws.BinaryMessage, buf) 67 | if err != nil { 68 | c.Close() 69 | c.die = true 70 | return 71 | } 72 | } 73 | } 74 | 75 | func broacast() { 76 | clients := make(map[*WsClient]*WsClient, 0) 77 | 78 | for { 79 | data := <- bufCh 80 | //Vln(5, "[dbg]broacast()", len(data)) 81 | for _, c := range clients { 82 | err := c.Send(data) 83 | if err != nil { 84 | delete(clients, c) 85 | Vln(3, "[ws][client]removed", c.RemoteAddr(), len(clients)) 86 | } 87 | } 88 | for len(newclients) > 0 { 89 | c := <-newclients 90 | clients[c] = c 91 | Vln(3, "[ws][client]added", c.RemoteAddr()) 92 | } 93 | } 94 | } 95 | 96 | func wsHandler(w http.ResponseWriter, r *http.Request) { 97 | c, err := upgrader.Upgrade(w, r, nil) 98 | if err != nil { 99 | Vln(2, "[ws]upgrade failed:", err) 100 | return 101 | } 102 | defer c.Close() 103 | 104 | Vln(3, "[ws][client]connect", c.RemoteAddr()) 105 | client := NewWsClient(c) 106 | newclients <- client 107 | 108 | client.worker() 109 | 110 | Vln(3, "[ws][client]disconnect", c.RemoteAddr()) 111 | } 112 | 113 | func main() { 114 | log.SetFlags(log.Ldate|log.Ltime) 115 | flag.Parse() 116 | 117 | upgrader.EnableCompression = *wsComp 118 | Vf(1, "ws EnableCompression = %v\n", *wsComp) 119 | Vf(1, "server Listen @ %v\n", *localAddr) 120 | Vf(1, "input image type = %v\n", *split) 121 | 122 | newclients = make(chan *WsClient, 16) 123 | bufCh = make(chan []byte, 1) 124 | go broacast() 125 | 126 | go connCam() 127 | 128 | http.HandleFunc("/ws", wsHandler) 129 | http.Handle("/", http.FileServer(http.Dir("./"))) 130 | 131 | err := http.ListenAndServe(*localAddr, nil) 132 | if err != nil { 133 | Vln(1, "server listen error:", err) 134 | } 135 | } 136 | 137 | func connCam() { 138 | markMap := map[string](func (r *bufio.Reader, buf []byte) (int, error)){ 139 | "jpg": readJPG, 140 | "png": readPNG, 141 | } 142 | 143 | decodeFn, ok := markMap[*split] 144 | if !ok { 145 | Vln(2, "[input][type]err:", *split) 146 | return 147 | } 148 | reader := bufio.NewReaderSize(os.Stdin, 8*1024*1024) 149 | buf := make([]byte, 8*1024*1024) 150 | //reader := os.Stdin 151 | for { 152 | n, err := decodeFn(reader, buf) 153 | if err != nil { 154 | Vln(2, "[pipe][recv]err:", err) 155 | return 156 | } 157 | 158 | Vln(5, "[dbg]connCam()", n, buf[:8]) 159 | pack := make([]byte, n, n) 160 | copy(pack, buf[0:n]) 161 | 162 | // broacast frame 163 | bufCh <- pack 164 | 165 | // do what you want with the frame 166 | // ... 167 | } 168 | } 169 | 170 | func readJPG(r *bufio.Reader, buf []byte) (int, error) { 171 | if n, err := io.ReadFull(r, buf[:2]); err != nil { 172 | return n, err 173 | } 174 | 175 | offset := 2 176 | if !bytes.Equal(buf[:2], []byte("\xFF\xD8")) { 177 | return offset, errors.New("may not JPG image") 178 | } 179 | 180 | for { 181 | s, err := r.ReadSlice('\xD9') 182 | if err != nil { 183 | return offset, err 184 | } 185 | 186 | copy(buf[offset:], s) 187 | offset += len(s) 188 | if bytes.HasSuffix(buf[:offset], []byte("\xFF\xD9")) { 189 | return offset, nil 190 | } 191 | } 192 | } 193 | 194 | func readPNG(r *bufio.Reader, buf []byte) (int, error) { 195 | if n, err := io.ReadFull(r, buf[:8]); err != nil { 196 | return n, err 197 | } 198 | 199 | offset := 8 200 | if !bytes.Equal(buf[:8], []byte("\x89PNG\x0D\x0A\x1A\x0A")) { 201 | return offset, errors.New("may not PNG image") 202 | } 203 | 204 | for { 205 | n, t, err := readPNGChunk(r, buf[offset:]) 206 | offset += n 207 | if err != nil { 208 | return offset, err 209 | } 210 | if bytes.Equal(t, []byte("IEND")) { 211 | return offset, nil 212 | } 213 | } 214 | } 215 | 216 | func readPNGChunk(r io.Reader, buf []byte) (int, []byte, error){ 217 | if n, err := io.ReadFull(r, buf[:4]); err != nil { //chunk data length 218 | return n, nil, err 219 | } 220 | dataLen := binary.BigEndian.Uint32(buf[0:4]) + 4 + 4 // chunk type & CRC length 221 | 222 | n, err := io.ReadFull(r, buf[4:4+4]) // chunk type 223 | if err != nil { 224 | return n+4, nil, err 225 | } 226 | chunkType := buf[4:4+4] 227 | 228 | n, err = io.ReadFull(r, buf[4+4:dataLen+4]) // chunk data & CRC 229 | if err != nil { 230 | return n+8, chunkType, err 231 | } 232 | //Vln(6, "ReadPipe:", dataLen, string(chunkType)) 233 | 234 | return n+8, chunkType, nil 235 | } 236 | 237 | func Vln(level int, v ...interface{}) { 238 | if level <= *verbosity { 239 | log.Println(v...) 240 | } 241 | } 242 | func Vf(level int, format string, v ...interface{}) { 243 | if level <= *verbosity { 244 | log.Printf(format, v...) 245 | } 246 | } 247 | 248 | --------------------------------------------------------------------------------