├── .dockerignore ├── .gitignore ├── CREDITS.md ├── Dockerfile ├── LICENSE ├── README.md ├── img2xterm ├── colorutil.go ├── framecache.go └── img2xterm.go ├── screenshot.png ├── server └── sshd │ └── sshd.go ├── sshcam.go ├── streaming.go └── webcam └── v4l2 ├── README.md ├── webcam.c ├── webcam.go ├── webcam.h └── webcam_wrapper.h /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rgb 2 | sshcam*.tar.bz 3 | sshcam 4 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | This project was inspired from: 4 | 5 | - [txtcam](https://github.com/dhotson/txtcam) 6 | - **Star Wars Movie in Telnet** (telnet://towel.blinkenlights.nl) 7 | - [img2xterm](https://github.com/rossy/img2xterm) 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM base/archlinux 2 | 3 | MAINTAINER kfei 4 | 5 | ENV GOPATH /go 6 | ENV PATH $GOPATH/bin:$PATH 7 | 8 | RUN pacman -Syy && pacman -S --noconfirm gcc git go \ 9 | && go get -u github.com/kfei/sshcam \ 10 | && cd $GOPATH/src/github.com/kfei/sshcam \ 11 | && go build \ 12 | && go install \ 13 | && pacman --noconfirm -R gcc git go \ 14 | && pacman --noconfirm -R $(pacman -Qdtq) 15 | 16 | VOLUME ["/.sshcam"] 17 | 18 | ENTRYPOINT ["/go/bin/sshcam"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Lin, Ke-fei (kfei). http://kfei.net 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sshcam 2 | 3 | Webcam live streaming in SSH terminal. 4 | 5 | [![ScreenShot](screenshot.png?raw=true)](http://youtu.be/pAa-pGda9kY) 6 | 7 | ## Quick Start 8 | 9 | To preview the stream, run *sshcam* without the `--server` argument: 10 | 11 | ```bash 12 | sshcam --color --size=1280x720 13 | ``` 14 | 15 | Note that you should run *sshcam* as an user with privilege to open 16 | `/dev/videoX`. 17 | 18 | Start the SSH server with all default settings: 19 | 20 | ```bash 21 | sshcam --server 22 | ``` 23 | 24 | Then on the client-side, run: 25 | 26 | ```bash 27 | ssh sshcam@your.server.ip -p 5566 # Default login: sshcam/p@ssw0rd 28 | ``` 29 | 30 | There are some configurable settings for server, have a look at `sshcam -h` for 31 | more information. As an example: 32 | 33 | ```bash 34 | sshcam --server --listen=127.0.0.1 --port=22222 \ 35 | --user=john --pass=nhoj \ 36 | --device=/dev/video0 --size=1280x720 \ 37 | --color --max-fps=2 38 | ``` 39 | 40 | ## Requirements 41 | 42 | - On the client-side, a `ssh` utility with 256-colors support is enough. 43 | - Video device is supported by 44 | [V4L2](https://www.kernel.org/doc/Documentation/video4linux/v4l2-framework.txt), 45 | which means the server is currently Linux only. 46 | 47 | ## Installation 48 | 49 | There are several ways to install *sshcam*. 50 | 51 | **Install binary from GitHub**: 52 | 53 | ```bash 54 | # Change to a valid string 55 | curl -sL https://github.com/kfei/sshcam/releases/download//sshcam-x64.tar.bz | tar xj 56 | mv sshcam /usr/local/bin/ 57 | ``` 58 | 59 | **Build from source** if you have a Go development environment: 60 | 61 | ```bash 62 | # Build passed on Go version 1.4 and GCC version 4.9.2 63 | go get -u github.com/kfei/sshcam 64 | cd $GOPATH/src/github.com/kfei/sshcam 65 | go build 66 | go install 67 | ``` 68 | 69 | **Build and run in Docker container**: 70 | 71 | ```bash 72 | git clone https://github.com/kfei/sshcam 73 | cd sshcam 74 | docker build -t sshcam . 75 | # After built, you can run sshcam via the Docker container 76 | # The privileged flag is for /dev/videoX access (FIXME) 77 | alias sshcam='docker run -it -p 5566:5566 --priviliged sshcam' 78 | ``` 79 | 80 | ## TODO 81 | 82 | There are still lots of interesting works to be done. Discussions and pull 83 | requests are both welcome. :) 84 | 85 | - **Port to other platforms**: Maybe by using OpenCV? 86 | - **Better performance**: Can the pixel rendering be more efficient? 87 | - **Even higher resolution**: Try Unicode quadrant block characters [2596 to 88 | 259F](http://www.alanwood.net/unicode/block_elements.html). 89 | 90 | ## License 91 | 92 | The MIT License (MIT) 93 | -------------------------------------------------------------------------------- /img2xterm/colorutil.go: -------------------------------------------------------------------------------- 1 | package img2xterm 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | var chromaWeight float64 = 1.0 8 | var rgbTable [256 * 3]uint8 9 | var yiqTable [256 * 3]float64 10 | var labTable [256 * 3]float64 11 | var valueRange [6]uint8 = [6]uint8{0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff} 12 | 13 | func xterm2RGB(color uint8, rgb []uint8) { 14 | if color < 232 { 15 | color -= 16 16 | rgb[0] = valueRange[(color/36)%6] 17 | rgb[1] = valueRange[(color/6)%6] 18 | rgb[2] = valueRange[color%6] 19 | } else { 20 | rgb[0] = 8 + (color-232)*10 21 | rgb[1] = rgb[0] 22 | rgb[2] = rgb[0] 23 | } 24 | } 25 | 26 | func srgb2YIQ(red, green, blue uint8, y, i, q *float64) { 27 | r := float64(red) / 255.0 28 | g := float64(green) / 255.0 29 | b := float64(blue) / 255.0 30 | 31 | *y = 0.299*r + 0.587*g + 0.144*b 32 | *i = (0.595716*r + -0.274453*g + -0.321263*b) * chromaWeight 33 | *q = (0.211456*r + -0.522591*g + 0.311135*b) * chromaWeight 34 | } 35 | 36 | func srgb2LAB(red, green, blue uint8, l, aa, bb *float64) { 37 | var r, g, b float64 38 | var rl, gl, bl float64 39 | var x, y, z float64 40 | var xn, yn, zn float64 41 | var fxn, fyn, fzn float64 42 | 43 | r, g, b = float64(red)/255.0, float64(green)/255.0, float64(blue)/255.0 44 | 45 | if r <= 0.4045 { 46 | rl = r / 12.92 47 | } else { 48 | rl = math.Pow((r+0.055)/1.055, 2.4) 49 | } 50 | if g <= 0.4045 { 51 | gl = g / 12.92 52 | } else { 53 | gl = math.Pow((g+0.055)/1.055, 2.4) 54 | } 55 | if b <= 0.4045 { 56 | bl = b / 12.92 57 | } else { 58 | bl = math.Pow((b+0.055)/1.055, 2.4) 59 | } 60 | 61 | x = 0.4124564*rl + 0.3575761*gl + 0.1804375*bl 62 | y = 0.2126729*rl + 0.7151522*gl + 0.0721750*bl 63 | z = 0.0193339*rl + 0.1191920*gl + 0.9503041*bl 64 | 65 | xn, yn, zn = x/0.95047, y, z/1.08883 66 | 67 | if xn > (216.0 / 24389.0) { 68 | fxn = math.Pow(xn, 1.0/3.0) 69 | } else { 70 | fxn = (841.0/108.0)*xn + (4.0 / 29.0) 71 | } 72 | 73 | if yn > (216.0 / 24389.0) { 74 | fyn = math.Pow(yn, 1.0/3.0) 75 | } else { 76 | fyn = (841.0/108.0)*yn + (4.0 / 29.0) 77 | } 78 | if zn > (216.0 / 24389.0) { 79 | fzn = math.Pow(zn, 1.0/3.0) 80 | } else { 81 | fzn = (841.0/108.0)*zn + (4.0 / 29.0) 82 | } 83 | 84 | *l = 116.0*fyn - 16.0 85 | *aa = (500.0 * (fxn - fyn)) * chromaWeight 86 | *bb = (200.0 * (fyn - fzn)) * chromaWeight 87 | } 88 | 89 | func cie94(l1, a1, b1, l2, a2, b2 float64) (distance float64) { 90 | const ( 91 | kl float64 = 1 92 | k1 float64 = 0.045 93 | k2 float64 = 0.015 94 | ) 95 | 96 | var c1 float64 = math.Sqrt(a1*a1 + b1*b1) 97 | var c2 float64 = math.Sqrt(a2*a2 + b2*b2) 98 | var dl float64 = l1 - l2 99 | var dc float64 = c1 - c2 100 | var da float64 = a1 - a2 101 | var db float64 = b1 - b2 102 | var dh float64 = math.Sqrt(da*da + db*db - dc*dc) 103 | 104 | var t1 float64 = dl / kl 105 | var t2 float64 = dc / (1 + k1*c1) 106 | var t3 float64 = dh / (1 + k2*c1) 107 | 108 | distance = math.Sqrt(t1*t1 + t2*t2 + t3*t3) 109 | return 110 | } 111 | 112 | func rgb2XtermCIE94(r, g, b uint8) (ret uint8) { 113 | var d, smallestDistance = math.MaxFloat64, math.MaxFloat64 114 | var l, aa, bb float64 115 | 116 | srgb2LAB(r, g, b, &l, &aa, &bb) 117 | 118 | for i := 16; i < 256; i++ { 119 | d = cie94(l, aa, bb, labTable[i*3], labTable[i*3+1], labTable[i*3+2]) 120 | if d < smallestDistance { 121 | smallestDistance = d 122 | ret = uint8(i) 123 | } 124 | } 125 | 126 | return 127 | } 128 | 129 | func rgb2XtermYIQ(r, g, b uint8) (ret uint8) { 130 | var d, smallestDistance = math.MaxFloat64, math.MaxFloat64 131 | var y, ii, q float64 132 | 133 | srgb2YIQ(r, g, b, &y, &ii, &q) 134 | 135 | for i := 16; i < 256; i++ { 136 | d = (y-yiqTable[i*3])*(y-yiqTable[i*3]) + 137 | (ii-yiqTable[i*3+1])*(ii-yiqTable[i*3+1]) + 138 | (q-yiqTable[i*3+2])*(q-yiqTable[i*3+2]) 139 | if d < smallestDistance { 140 | smallestDistance = d 141 | ret = uint8(i) 142 | } 143 | } 144 | 145 | return 146 | } 147 | 148 | func rgb2XtermRGB(r, g, b uint8) (ret uint8) { 149 | var d, smallestDistance = math.MaxInt64, math.MaxInt64 150 | 151 | for i := 16; i < 256; i++ { 152 | dr, dg, db := int(rgbTable[i*3]-r), int(rgbTable[i*3+1]-g), int(rgbTable[i*3+2]-b) 153 | d = dr*dr + dg*dg + db*db 154 | if d < smallestDistance { 155 | smallestDistance = d 156 | ret = uint8(i) 157 | } 158 | } 159 | 160 | return 161 | } 162 | 163 | func init() { 164 | // TODO: Make these tables initialize dynamically 165 | var rgb []uint8 = []uint8{0, 0, 0} 166 | var l, a, b float64 167 | var y, i, q float64 168 | 169 | for j := 16; j < 256; j++ { 170 | xterm2RGB(uint8(j), rgb) 171 | 172 | // Initial RGB table 173 | rgbTable[j*3] = rgb[0] 174 | rgbTable[j*3+1] = rgb[1] 175 | rgbTable[j*3+2] = rgb[2] 176 | 177 | // Initial LAB table 178 | srgb2LAB(rgb[0], rgb[1], rgb[2], &l, &a, &b) 179 | labTable[j*3] = l 180 | labTable[j*3+1] = a 181 | labTable[j*3+2] = b 182 | 183 | // Initial YIQ table 184 | srgb2YIQ(rgb[0], rgb[1], rgb[2], &y, &i, &q) 185 | yiqTable[j*3] = y 186 | yiqTable[j*3+1] = i 187 | yiqTable[j*3+2] = q 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /img2xterm/framecache.go: -------------------------------------------------------------------------------- 1 | package img2xterm 2 | 3 | const ( 4 | cacheXSize int = 360 5 | cacheYSize int = 120 6 | ) 7 | 8 | type FrameCache [cacheXSize][cacheYSize][2]uint8 9 | 10 | var fCache FrameCache 11 | 12 | func ClearCache() { 13 | // TODO: Can this be more efficient? 14 | for i := 0; i < cacheXSize; i++ { 15 | for j := 0; j < cacheYSize; j++ { 16 | fCache[i][j][0] = 0 17 | fCache[i][j][1] = 0 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /img2xterm/img2xterm.go: -------------------------------------------------------------------------------- 1 | package img2xterm 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | colorUndef = iota 11 | colorTransparent = iota 12 | ) 13 | 14 | type Config struct { 15 | // Specify width and height of the input image 16 | Width, Height int 17 | 18 | // True to draw colorized image 19 | // False to draw in grayscale 20 | Colorful bool 21 | 22 | // How to compute the distance of colors 23 | // 'rgb': Use simple RGB linear distance 24 | // 'yiq': Still linear distance, but in YIQ colorspace (default) 25 | // 'cie94': Use the CIE94 algorithm, which consumes CPU more 26 | DistanceAlgorithm string 27 | } 28 | 29 | var oldfg, oldbg uint8 = colorUndef, colorUndef 30 | var sequence string 31 | 32 | func rawRGB2Pixels(raw []byte) (ret [][3]byte) { 33 | for cur := 0; cur < len(raw); cur += 3 { 34 | pixel := [3]byte{raw[cur], raw[cur+1], raw[cur+2]} 35 | ret = append(ret, pixel) 36 | } 37 | return 38 | } 39 | 40 | func rawRGB2BrightnessPixels(raw []byte) (ret []float64) { 41 | for cur := 0; cur < len(raw); cur += 3 { 42 | r, g, b := raw[cur], raw[cur+1], raw[cur+2] 43 | bri := (float64(r)*0.299 + float64(g)*0.587 + float64(b)*0.114) / 255.0 44 | ret = append(ret, bri) 45 | } 46 | return 47 | } 48 | 49 | // DrawRGB draws RGB byte-array to terminal. 50 | // It takes width and height for recognizing the raw RGB byte-array, and 51 | // colorful to draw blocks with colors. 52 | func DrawRGB(raw []byte, config *Config) { 53 | var colorful bool = false 54 | 55 | // Prepare drawing settings 56 | width, height := config.Width, config.Height 57 | rgb2Xterm := rgb2XtermYIQ 58 | if config.Colorful { 59 | colorful = true 60 | switch config.DistanceAlgorithm { 61 | case "rgb": 62 | rgb2Xterm = rgb2XtermRGB 63 | case "cie94": 64 | rgb2Xterm = rgb2XtermCIE94 65 | default: 66 | rgb2Xterm = rgb2XtermYIQ 67 | } 68 | } 69 | 70 | var color1, color2, brightness1, brightness2 uint8 71 | if colorful { 72 | // Draw image with color 73 | pixels := rawRGB2Pixels(raw) 74 | for y := 0; y < height; y += 2 { 75 | for x := 0; x < width; x++ { 76 | // Compute the color of upper block 77 | r1 := pixels[y*width+x][0] 78 | g1 := pixels[y*width+x][1] 79 | b1 := pixels[y*width+x][2] 80 | color1 = rgb2Xterm(r1, g1, b1) 81 | 82 | // Compute the color of lower block 83 | if (y + 1) < height { 84 | r2 := pixels[(y+1)*width+x][0] 85 | g2 := pixels[(y+1)*width+x][1] 86 | b2 := pixels[(y+1)*width+x][2] 87 | color2 = rgb2Xterm(r2, g2, b2) 88 | } else { 89 | color2 = colorTransparent 90 | } 91 | 92 | // Draw one pixel (if needed) 93 | if color1 != fCache[x][y/2][0] || color2 != fCache[x][y/2][1] { 94 | dot(x, y/2, color1, color2) 95 | fCache[x][y/2][0], fCache[x][y/2][1] = color1, color2 96 | } 97 | } 98 | if (y + 2) < height { 99 | fmt.Println(sequence) 100 | sequence = "" 101 | } 102 | } 103 | } else { 104 | // Draw image in grayscale 105 | pixels := rawRGB2BrightnessPixels(raw) 106 | for y := 0; y < height; y += 2 { 107 | for x := 0; x < width; x++ { 108 | brightness1 = uint8(pixels[y*width+x]*23) + 232 109 | if (y + 1) < height { 110 | brightness2 = uint8(pixels[(y+1)*width+x]*23) + 232 111 | } else { 112 | brightness2 = colorTransparent 113 | } 114 | // Draw one pixel (if needed) 115 | if brightness1 != fCache[x][y/2][0] || brightness2 != fCache[x][y/2][1] { 116 | dot(x, y/2, brightness1, brightness2) 117 | fCache[x][y/2][0], fCache[x][y/2][1] = brightness1, brightness2 118 | } 119 | } 120 | if (y + 2) < height { 121 | fmt.Println(sequence) 122 | sequence = "" 123 | } 124 | } 125 | } 126 | } 127 | 128 | func dot(x, y int, color1, color2 uint8) { 129 | // Move cursor 130 | sequence += "\033[" + strconv.Itoa(y+1) + ";" + strconv.Itoa(x+1) + "H" 131 | 132 | fg, bg := oldfg, oldbg 133 | 134 | // The lower half block "▄" 135 | var str = "\xe2\x96\x84" 136 | 137 | if color1 == color2 { 138 | bg = color1 139 | str = " " 140 | } else if color2 == colorTransparent { 141 | // The upper half block "▀" 142 | str = "\xe2\x96\x80" 143 | bg, fg = color2, color1 144 | } else { 145 | bg, fg = color1, color2 146 | } 147 | 148 | if bg != oldbg { 149 | if bg == colorTransparent { 150 | sequence += "\033[49m" 151 | } else { 152 | sequence += "\033[48;5;" + strconv.Itoa(int(bg)) + "m" 153 | } 154 | } 155 | 156 | if fg != oldfg { 157 | if fg == colorUndef { 158 | sequence += "\033[39m" 159 | } else { 160 | sequence += "\033[38;5;" + strconv.Itoa(int(fg)) + "m" 161 | } 162 | } 163 | 164 | oldbg, oldfg = bg, fg 165 | 166 | sequence += str 167 | } 168 | 169 | func floatMod(x, y float64) float64 { 170 | return x - y*math.Floor(x/y) 171 | } 172 | 173 | func floatMin(x, y float64) float64 { 174 | if x-y > 0 { 175 | return y 176 | } 177 | return x 178 | } 179 | 180 | // AsciiDrawRGB draws a RGB byte-array to terminal. 181 | // It uses only full block characters and without colors and framecache. 182 | func AsciiDrawRGB(raw []byte, config *Config) { 183 | var chr string 184 | width, height := config.Width, config.Height 185 | pixels := rawRGB2BrightnessPixels(raw) 186 | for y := 0; y < height; y++ { 187 | for x := 0; x < width; x++ { 188 | brightness := pixels[y*width+x] 189 | bg := brightness*23 + 232 190 | fg := floatMin(255, bg+1) 191 | mod := floatMod(bg, 1.0) 192 | 193 | switch { 194 | case mod < 0.2: 195 | chr = " " 196 | case mod < 0.4: 197 | chr = "░" 198 | case mod < 0.6: 199 | chr = "▒" 200 | case mod < 0.8: 201 | bg, fg = fg, bg 202 | chr = "▒" 203 | default: 204 | bg, fg = fg, bg 205 | chr = "░" 206 | } 207 | 208 | fmt.Printf("\033[48;5;%dm\033[38;5;%dm%s", int(bg), int(fg), chr) 209 | } 210 | if (y + 1) < height { 211 | fmt.Print("\n") 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfei/sshcam/cdccac039894f6bc4f52ee7413fedfffd751a45b/screenshot.png -------------------------------------------------------------------------------- /server/sshd/sshd.go: -------------------------------------------------------------------------------- 1 | package sshd 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/binary" 8 | "encoding/pem" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "net" 14 | "os" 15 | "os/exec" 16 | "os/user" 17 | "path/filepath" 18 | "strings" 19 | "sync" 20 | "syscall" 21 | "unsafe" 22 | 23 | "github.com/kr/pty" 24 | "golang.org/x/crypto/ssh" 25 | ) 26 | 27 | func generatePrivateKey(keyFile string) []byte { 28 | log.Println("Generating new key pair...") 29 | key, err := rsa.GenerateKey(rand.Reader, 2048) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | keyBytes := x509.MarshalPKCS1PrivateKey(key) 35 | pemBytes := pem.EncodeToMemory(&pem.Block{ 36 | Type: "RSA PRIVATE KEY", Bytes: keyBytes}) 37 | err = ioutil.WriteFile(keyFile, pemBytes, 0600) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Println("Saved to ", keyFile) 42 | 43 | return pemBytes 44 | } 45 | 46 | func readLocalPrivateKey() ssh.Signer { 47 | usr, err := user.Current() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | err = os.MkdirAll(filepath.Join(usr.HomeDir, ".sshcam"), os.ModeDir) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | keyFile := filepath.Join(usr.HomeDir, ".sshcam", "id_rsa") 58 | 59 | privateBytes, err := ioutil.ReadFile(keyFile) 60 | if err != nil { 61 | log.Println("Private key for sshcam server does not exists") 62 | privateBytes = generatePrivateKey(keyFile) 63 | } 64 | 65 | private, err := ssh.ParsePrivateKey(privateBytes) 66 | if err != nil { 67 | log.Fatal("Failed to parse private key") 68 | } 69 | 70 | return private 71 | } 72 | 73 | func Run(user, pass, host, port string, sshcamArgs []string) { 74 | config := &ssh.ServerConfig{ 75 | NoClientAuth: false, 76 | PasswordCallback: func(c ssh.ConnMetadata, 77 | password []byte) (*ssh.Permissions, error) { 78 | if c.User() == user && string(password) == pass { 79 | return nil, nil 80 | } 81 | return nil, fmt.Errorf("Password rejected for %q", c.User()) 82 | }, 83 | } 84 | 85 | config.AddHostKey(readLocalPrivateKey()) 86 | 87 | bindAddress := strings.Join([]string{host, port}, ":") 88 | listener, err := net.Listen("tcp", bindAddress) 89 | if err != nil { 90 | log.Fatal("Failed to listen on " + bindAddress) 91 | } 92 | 93 | // Accepting connections 94 | log.Print("Listening on " + bindAddress + "...") 95 | for { 96 | tcpConn, err := listener.Accept() 97 | if err != nil { 98 | log.Printf("Failed to accept incoming connections (%s)", err) 99 | continue 100 | } 101 | // Handshaking 102 | sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config) 103 | if err != nil { 104 | log.Printf("Failed to handshake (%s)", err) 105 | continue 106 | } 107 | 108 | log.Printf("New ssh connection established from %s (%s)", 109 | sshConn.RemoteAddr(), 110 | sshConn.ClientVersion()) 111 | 112 | // Print incoming out-of-band Requests 113 | go handleRequests(reqs) 114 | // Accept all channels 115 | go handleChannels(chans, sshcamArgs) 116 | } 117 | } 118 | 119 | func handleRequests(reqs <-chan *ssh.Request) { 120 | for req := range reqs { 121 | log.Printf("Recieved out-of-band request: %+v", req) 122 | } 123 | } 124 | 125 | func handleChannels(chans <-chan ssh.NewChannel, sshcamArgs []string) { 126 | // Service the incoming Channel channel. 127 | for newChannel := range chans { 128 | if t := newChannel.ChannelType(); t != "session" { 129 | newChannel.Reject(ssh.UnknownChannelType, 130 | fmt.Sprintf("Unknown channel type: %s", t)) 131 | continue 132 | } 133 | channel, requests, err := newChannel.Accept() 134 | if err != nil { 135 | log.Printf("Could not accept channel (%s)", err) 136 | continue 137 | } 138 | 139 | // Allocate a terminal for this channel 140 | log.Print("Creating pty...") 141 | 142 | // Can this always work without PATH specified? 143 | c := exec.Command("sshcam", sshcamArgs...) 144 | 145 | f, err := pty.Start(c) 146 | if err != nil { 147 | log.Printf("Could not start pty (%s)", err) 148 | continue 149 | } 150 | 151 | // Teardown session 152 | var once sync.Once 153 | close := func() { 154 | channel.Close() 155 | _, err := c.Process.Wait() 156 | if err != nil { 157 | log.Printf("Failed to exit session (%s)", err) 158 | } 159 | log.Printf("Session closed") 160 | } 161 | 162 | // Pipe session to sshcam and visa versa 163 | go func() { 164 | io.Copy(channel, f) 165 | once.Do(close) 166 | }() 167 | go func() { 168 | io.Copy(f, channel) 169 | once.Do(close) 170 | }() 171 | 172 | // Deal with session requests 173 | go func(in <-chan *ssh.Request) { 174 | for req := range in { 175 | ok := false 176 | switch req.Type { 177 | case "shell": 178 | // Don't accept any commands (payload) 179 | if len(req.Payload) == 0 { 180 | ok = true 181 | } 182 | case "pty-req": 183 | // Responding 'ok' here will let the client 184 | // know we have a pty ready for input 185 | ok = true 186 | // Parse body... 187 | termLen := req.Payload[3] 188 | termEnv := string(req.Payload[4 : termLen+4]) 189 | w, h := parseDims(req.Payload[termLen+4:]) 190 | SetWinsize(f.Fd(), w, h) 191 | log.Printf("pty-req '%s'", termEnv) 192 | case "window-change": 193 | w, h := parseDims(req.Payload) 194 | SetWinsize(f.Fd(), w, h) 195 | continue 196 | } 197 | if !ok { 198 | log.Printf("Declining %s request...", req.Type) 199 | } 200 | req.Reply(ok, nil) 201 | } 202 | }(requests) 203 | } 204 | } 205 | 206 | // Extracts two uint32s from the provided buffer 207 | func parseDims(b []byte) (uint32, uint32) { 208 | w := binary.BigEndian.Uint32(b) 209 | h := binary.BigEndian.Uint32(b[4:]) 210 | return w, h 211 | } 212 | 213 | // Winsize stores the Height and Width of a terminal 214 | type Winsize struct { 215 | Height uint16 216 | Width uint16 217 | } 218 | 219 | // SetWinsize sets the size of the given pty 220 | func SetWinsize(fd uintptr, w, h uint32) { 221 | log.Printf("Window resize %dx%d", w, h) 222 | ws := &Winsize{Width: uint16(w), Height: uint16(h)} 223 | syscall.Syscall(syscall.SYS_IOCTL, 224 | fd, 225 | uintptr(syscall.TIOCSWINSZ), 226 | uintptr(unsafe.Pointer(ws))) 227 | } 228 | -------------------------------------------------------------------------------- /sshcam.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | 7 | flag "github.com/docker/docker/pkg/mflag" 8 | "github.com/kfei/sshcam/server/sshd" 9 | webcam "github.com/kfei/sshcam/webcam/v4l2" 10 | ) 11 | 12 | type Size struct { 13 | Width, Height int 14 | } 15 | 16 | var ( 17 | h, server, colorful, asciiOnly bool 18 | port, maxFPS int 19 | listen, device, sizeFlag, user, pass, distanceAlgorithm string 20 | size Size 21 | ) 22 | 23 | func init() { 24 | // Arguments for ssh server 25 | flag.BoolVar(&h, []string{"h", "#help", "-help"}, false, 26 | "display this help message") 27 | flag.BoolVar(&server, []string{"s", "-server"}, false, 28 | "start the server") 29 | flag.StringVar(&listen, []string{"l", "-listen"}, "0.0.0.0", 30 | "start the server") 31 | flag.IntVar(&port, []string{"p", "-port"}, 5566, 32 | "port to listen") 33 | flag.StringVar(&user, []string{"-user"}, "sshcam", 34 | "username for SSH login") 35 | flag.StringVar(&pass, []string{"-pass"}, "p@ssw0rd", 36 | "password for SSH login") 37 | 38 | // Arguments for img2xterm 39 | flag.BoolVar(&colorful, []string{"c", "-color"}, false, 40 | "turn on color") 41 | flag.BoolVar(&asciiOnly, []string{"-ascii-only"}, false, 42 | "fallback to use ASCII's full block characters") 43 | flag.StringVar(&distanceAlgorithm, []string{"-color-algorithm"}, "yiq", 44 | "algorithm use to compute colors. Available options are:\n"+ 45 | "'rgb': simple linear distance in RGB colorspace\n"+ 46 | "'yiq': simple linear distance in YIQ colorspace (the default)\n"+ 47 | "'cie94': use the CIE94 formula") 48 | flag.IntVar(&maxFPS, []string{"-max-fps"}, 4, 49 | "limit the maximum FPS") 50 | flag.StringVar(&device, []string{"-device"}, "/dev/video0", 51 | "the webcam device to open") 52 | flag.StringVar(&sizeFlag, []string{"-size"}, "640x480", 53 | "image dimension, must be supported by the device") 54 | 55 | flag.Parse() 56 | size = wxh2Size(sizeFlag) 57 | } 58 | 59 | func main() { 60 | switch { 61 | case h: 62 | flag.PrintDefaults() 63 | case server: 64 | // TODO: Better way to copy these arguments to sshd? 65 | sshcamArgs := []string{ 66 | "--device=" + device, 67 | "--size=" + sizeFlag, 68 | "--color-algorithm=" + distanceAlgorithm, 69 | "--max-fps=" + strconv.Itoa(maxFPS)} 70 | if colorful { 71 | sshcamArgs = append(sshcamArgs, "--color") 72 | } 73 | if asciiOnly { 74 | sshcamArgs = append(sshcamArgs, "--ascii-only") 75 | } 76 | sshd.Run(user, pass, listen, strconv.Itoa(port), sshcamArgs) 77 | default: 78 | var wg sync.WaitGroup 79 | 80 | // Initialize the webcam device 81 | webcam.OpenWebcam(device, size.Width, size.Height) 82 | defer webcam.CloseWebcam() 83 | 84 | // Start the TTY size updater goroutine 85 | ttyStatus := updateTTYSize() 86 | 87 | // Fire the drawing goroutine 88 | wg.Add(1) 89 | go streaming(ttyStatus, &wg) 90 | wg.Wait() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /streaming.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "os/signal" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/kfei/sshcam/img2xterm" 15 | webcam "github.com/kfei/sshcam/webcam/v4l2" 16 | ) 17 | 18 | var ttySize Size = Size{0, 0} 19 | 20 | func wxh2Size(s string) Size { 21 | splits := [...]string{"x", "*", " "} 22 | for i := range splits { 23 | splitted := strings.Split(s, splits[i]) 24 | if len(splitted) != 2 { 25 | continue 26 | } 27 | w, err1 := strconv.Atoi(splitted[0]) 28 | h, err2 := strconv.Atoi(splitted[1]) 29 | if err1 == nil && err2 == nil { 30 | return Size{w, h} 31 | } 32 | } 33 | log.Println("Invalid argument: --size, fallback to default...") 34 | return wxh2Size("640x480") 35 | } 36 | 37 | func clearScreen() { 38 | // TODO: Use terminfo 39 | fmt.Print("\033[2J") 40 | } 41 | 42 | func restoreScreen() { 43 | // TODO: Use terminfo 44 | seq := "\033[" + strconv.Itoa(ttySize.Height) + ";1H\033[39m\033[49m\n" 45 | fmt.Print(seq) 46 | } 47 | 48 | func resetCursor() { 49 | // TODO: Use terminfo 50 | fmt.Print("\033[00H") 51 | } 52 | 53 | func updateTTYSize() <-chan string { 54 | ttyStatus := make(chan string) 55 | go func() { 56 | for { 57 | // TODO: Use syscall.Syscall? 58 | cmd := exec.Command("stty", "size") 59 | cmd.Stdin = os.Stdin 60 | out, err := cmd.Output() 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | // An example will be "25 \n80" 66 | curSize := strings.TrimSuffix(string(out), "\n") 67 | 68 | // If TTY size has been changed, clear the frame cache 69 | oldSize := strconv.Itoa(ttySize.Height) + " " + strconv.Itoa(ttySize.Width) 70 | if curSize != oldSize && oldSize != "0 0" { 71 | go img2xterm.ClearCache() 72 | resetCursor() 73 | clearScreen() 74 | } 75 | 76 | ttyStatus <- curSize 77 | 78 | // Simulate a limit of FPS 79 | sleepDuration := time.Duration(1000 / maxFPS) 80 | time.Sleep(sleepDuration * time.Millisecond) 81 | } 82 | }() 83 | return ttyStatus 84 | } 85 | 86 | func grabRGBPixels(ttySize Size, wInc, hInc int) (ret []byte) { 87 | rgbArray := webcam.GrabFrame() 88 | // Check the image size actually captured by webcam 89 | if size.Width*size.Height*3 > len(rgbArray) { 90 | log.Fatal("Pixels conversion failed. Have you specified a size " + 91 | "which is not supported by the webcam?") 92 | } 93 | 94 | // Assuming the captured image is larger than terminal size 95 | if ttySize.Width*ttySize.Height*hInc > len(rgbArray)/3 { 96 | log.Fatal("Capture size too small.") 97 | } 98 | 99 | // TODO: Improve this inefficient and loosy algorithm 100 | skipX, skipY := size.Width/ttySize.Width, size.Height/(ttySize.Height*hInc) 101 | for y := 0; y < ttySize.Height*hInc; y++ { 102 | for x := 0; x < ttySize.Width; x++ { 103 | cur := size.Width*3*y*skipY + 3*x*skipX 104 | ret = append(ret, rgbArray[cur], rgbArray[cur+1], rgbArray[cur+2]) 105 | } 106 | } 107 | return 108 | } 109 | 110 | func streaming(ttyStatus <-chan string, wg *sync.WaitGroup) { 111 | var interrupt bool = false 112 | defer wg.Done() 113 | 114 | // Signal handling for normally exit 115 | sigchan := make(chan os.Signal, 1) 116 | signal.Notify(sigchan, os.Interrupt) 117 | go func() { 118 | <-sigchan 119 | interrupt = true 120 | }() 121 | 122 | // Prepare settings for imgxterm 123 | config := &img2xterm.Config{ 124 | Colorful: colorful, 125 | DistanceAlgorithm: distanceAlgorithm, 126 | } 127 | 128 | log.Println("Start streaming, press Ctrl-c to exit...") 129 | time.Sleep(1500 * time.Millisecond) 130 | clearScreen() 131 | 132 | for !interrupt { 133 | // Update TTY size before every draw (synchronous) 134 | curSize := strings.Split(<-ttyStatus, " ") 135 | ttySize.Height, _ = strconv.Atoi(curSize[0]) 136 | ttySize.Width, _ = strconv.Atoi(curSize[1]) 137 | 138 | resetCursor() 139 | 140 | // Fetch image from webcam and call img2xterm to draw 141 | if asciiOnly { 142 | rgbRaw := grabRGBPixels(ttySize, 1, 1) 143 | config.Width, config.Height = ttySize.Width, ttySize.Height 144 | img2xterm.AsciiDrawRGB(rgbRaw, config) 145 | } else { 146 | rgbRaw := grabRGBPixels(ttySize, 1, 2) 147 | config.Width, config.Height = ttySize.Width, ttySize.Height*2 148 | img2xterm.DrawRGB(rgbRaw, config) 149 | } 150 | } 151 | 152 | restoreScreen() 153 | log.Println("Exiting...") 154 | } 155 | -------------------------------------------------------------------------------- /webcam/v4l2/README.md: -------------------------------------------------------------------------------- 1 | # webcam/v4l2 2 | 3 | This package provides a Go wrapper to control V4L2 webcams. The C code was 4 | originally a fork from [wthielen](https://github.com/wthielen/Webcam). 5 | -------------------------------------------------------------------------------- /webcam/v4l2/webcam.c: -------------------------------------------------------------------------------- 1 | #include "webcam.h" 2 | #include 3 | 4 | /** 5 | * Keeping tabs on opened webcam devices 6 | */ 7 | static webcam_t *_w[16] = { 8 | NULL, NULL, NULL, NULL, 9 | NULL, NULL, NULL, NULL, 10 | NULL, NULL, NULL, NULL, 11 | NULL, NULL, NULL, NULL 12 | }; 13 | 14 | /** 15 | * Private sigaction to catch segmentation fault 16 | */ 17 | static struct sigaction *sa; 18 | 19 | /** 20 | * Private function for successfully ioctl-ing the v4l2 device 21 | */ 22 | static int _ioctl(int fh, int request, void *arg) 23 | { 24 | int r; 25 | 26 | do { 27 | r = ioctl(fh, request, arg); 28 | } while (-1 == r && EINTR == errno); 29 | 30 | return r; 31 | } 32 | 33 | /** 34 | * Private function to clamp a double value to the nearest int 35 | * between 0 and 255 36 | */ 37 | static uint8_t clamp(double x) 38 | { 39 | int r = x; 40 | 41 | if (r < 0) return 0; 42 | else if (r > 255) return 255; 43 | 44 | return r; 45 | } 46 | 47 | /** 48 | * Handler for segmentation faults 49 | * This should go through all the opened webcams in _w and 50 | * clean them up. 51 | */ 52 | static void handler(int sig, siginfo_t *si, void *unused) 53 | { 54 | int i = 0; 55 | fprintf(stderr, "[v4l2] A segmentation fault occured. Cleaning up...\n"); 56 | 57 | for(i = 0; i < 16; i++) { 58 | if (_w[i] == NULL) continue; 59 | 60 | // If webcam is streaming, unlock the mutex, and stop streaming 61 | if (_w[i]->streaming) { 62 | pthread_mutex_unlock(&_w[i]->mtx_frame); 63 | webcam_stream(_w[i], false); 64 | } 65 | webcam_close(_w[i]); 66 | } 67 | 68 | exit(EXIT_FAILURE); 69 | } 70 | 71 | /** 72 | * Private function to convert a YUYV buffer to a RGB frame and store it 73 | * within the given buffer structure 74 | * 75 | * http://linuxtv.org/downloads/v4l-dvb-apis/colorspaces.html 76 | */ 77 | static void convertToRGB(struct buffer buf, struct buffer *frame) 78 | { 79 | size_t i; 80 | uint8_t y, u, v; 81 | 82 | int uOffset = 0; 83 | int vOffset = 0; 84 | 85 | double R, G, B; 86 | double Y, Pb, Pr; 87 | 88 | // Initialize frame 89 | if (frame->start == NULL) { 90 | frame->length = buf.length / 2 * 3; 91 | frame->start = calloc(frame->length, sizeof(char)); 92 | } 93 | 94 | // Go through the YUYV buffer and calculate RGB pixels 95 | for (i = 0; i < buf.length; i += 2) 96 | { 97 | uOffset = (i % 4 == 0) ? 1 : -1; 98 | vOffset = (i % 4 == 2) ? 1 : -1; 99 | 100 | y = buf.start[i]; 101 | u = (i + uOffset > 0 && i + uOffset < buf.length) ? buf.start[i + uOffset] : 0x80; 102 | v = (i + vOffset > 0 && i + vOffset < buf.length) ? buf.start[i + vOffset] : 0x80; 103 | 104 | Y = (255.0 / 219.0) * (y - 0x10); 105 | Pb = (255.0 / 224.0) * (u - 0x80); 106 | Pr = (255.0 / 224.0) * (v - 0x80); 107 | 108 | R = 1.0 * Y + 0.000 * Pb + 1.402 * Pr; 109 | G = 1.0 * Y + 0.344 * Pb - 0.714 * Pr; 110 | B = 1.0 * Y + 1.772 * Pb + 0.000 * Pr; 111 | 112 | frame->start[i / 2 * 3 ] = clamp(R); 113 | frame->start[i / 2 * 3 + 1] = clamp(G); 114 | frame->start[i / 2 * 3 + 2] = clamp(B); 115 | } 116 | } 117 | 118 | /** 119 | * Private function to equalize the Y-histogram for contrast 120 | * using a cumulative distribution function 121 | * 122 | * Thought this would fix the colors in the first instance, 123 | * but it did not. Nevertheless a good function to keep. 124 | * 125 | * http://en.wikipedia.org/wiki/Histogram_equalization 126 | */ 127 | static void equalize(struct buffer *buf) 128 | { 129 | size_t i; 130 | uint16_t depth = 1 << 8; 131 | uint8_t value; 132 | 133 | size_t *histogram = calloc(depth, sizeof(size_t)); 134 | size_t *cdf = calloc(depth, sizeof(size_t)); 135 | size_t cdf_min = 0; 136 | 137 | // Skip CbCr components 138 | for (i = 0; i < buf->length; i += 2) 139 | { 140 | histogram[buf->start[i]]++; 141 | } 142 | 143 | // Create cumulative distribution 144 | for (i = 0; i < depth; i++) { 145 | cdf[i] = 0 == i ? histogram[i] : cdf[i - 1] + histogram[i]; 146 | if (cdf_min == 0 && cdf[i] > 0) cdf_min = cdf[i]; 147 | } 148 | 149 | // Equalize the Y values 150 | for (i = 0; i < buf->length; i += 2) { 151 | value = buf->start[i]; 152 | buf->start[i] = 1.0 * (cdf[value] - cdf_min) / (buf->length / 2 - cdf_min) * (depth - 1); 153 | } 154 | } 155 | 156 | /** 157 | * Open the webcam on the given device and return a webcam 158 | * structure. 159 | */ 160 | struct webcam *webcam_open(const char *dev) 161 | { 162 | struct stat st; 163 | 164 | struct v4l2_capability cap; 165 | struct v4l2_format fmt; 166 | 167 | uint16_t min; 168 | 169 | int fd; 170 | struct webcam *w; 171 | 172 | // Prepare signal handler if not yet 173 | if (sa == NULL) { 174 | sa = calloc(1, sizeof(struct sigaction)); 175 | sa->sa_flags = SA_SIGINFO; 176 | sigemptyset(&sa->sa_mask); 177 | sa->sa_sigaction = handler; 178 | sigaction(SIGSEGV, sa, NULL); 179 | } 180 | 181 | // Check if the device path exists 182 | if (-1 == stat(dev, &st)) { 183 | fprintf(stderr, "[v4l2] Cannot identify '%s': %d, %s\n", 184 | dev, errno, strerror(errno)); 185 | return NULL; 186 | } 187 | 188 | // Should be a character device 189 | if (!S_ISCHR(st.st_mode)) { 190 | fprintf(stderr, "[v4l2] %s is not a char device\n", dev); 191 | return NULL; 192 | } 193 | 194 | // Create a file descriptor 195 | fd = open(dev, O_RDWR | O_NONBLOCK, 0); 196 | if (-1 == fd) { 197 | fprintf(stderr, "[v4l2] Cannot open'%s': %d, %s\n", 198 | dev, errno, strerror(errno)); 199 | return NULL; 200 | } 201 | 202 | // Query the webcam capabilities 203 | if (-1 == _ioctl(fd, VIDIOC_QUERYCAP, &cap)) { 204 | if (EINVAL == errno) { 205 | fprintf(stderr, "[v4l2] %s is no V4L2 device\n", dev); 206 | return NULL; 207 | } else { 208 | fprintf(stderr, "[v4l2] %s: could not fetch video capabilities\n", dev); 209 | return NULL; 210 | } 211 | } 212 | 213 | // Needs to be a capturing device 214 | if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { 215 | fprintf(stderr, "[v4l2] %s is no video capture device\n", dev); 216 | return NULL; 217 | } 218 | 219 | // Prepare webcam structure 220 | w = calloc(1, sizeof(struct webcam)); 221 | w->fd = fd; 222 | w->name = strdup(dev); 223 | w->frame.start = NULL; 224 | w->frame.length = 0; 225 | pthread_mutex_init(&w->mtx_frame, NULL); 226 | 227 | // Initialize buffers 228 | w->nbuffers = 0; 229 | w->buffers = NULL; 230 | 231 | // Store webcam in _w 232 | int i = 0; 233 | for(; i < 16; i++) { 234 | if (_w[i] == NULL) { 235 | _w[i] = w; 236 | break; 237 | } 238 | } 239 | 240 | // Request supported formats 241 | struct v4l2_fmtdesc fmtdesc; 242 | uint32_t idx = 0; 243 | char *pixelformat = calloc(5, sizeof(char)); 244 | for(;;) { 245 | fmtdesc.index = idx; 246 | fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 247 | 248 | if (-1 == _ioctl(w->fd, VIDIOC_ENUM_FMT, &fmtdesc)) break; 249 | 250 | memset(w->formats[idx], 0, 5); 251 | memcpy(&w->formats[idx][0], &fmtdesc.pixelformat, 4); 252 | fprintf(stderr, "[v4l2] %s: Found format: %s - %s\n", w->name, w->formats[idx], fmtdesc.description); 253 | idx++; 254 | } 255 | 256 | return w; 257 | } 258 | 259 | /** 260 | * Closes the webcam 261 | * 262 | * Also releases the buffers, and frees up memory 263 | */ 264 | void webcam_close(webcam_t *w) 265 | { 266 | uint16_t i; 267 | 268 | // Clear frame 269 | free(w->frame.start); 270 | w->frame.length = 0; 271 | 272 | // Release memory-mapped buffers 273 | for (i = 0; i < w->nbuffers; i++) { 274 | munmap(w->buffers[i].start, w->buffers[i].length); 275 | } 276 | 277 | // Free allocated resources 278 | free(w->buffers); 279 | free(w->name); 280 | 281 | // Close the webcam file descriptor, and free the memory 282 | close(w->fd); 283 | free(w); 284 | } 285 | 286 | /** 287 | * Sets the webcam to capture at the given width and height 288 | */ 289 | int webcam_resize(webcam_t *w, uint16_t width, uint16_t height) 290 | { 291 | uint32_t i; 292 | struct v4l2_format fmt; 293 | struct v4l2_buffer buf; 294 | 295 | // Use YUYV as default for now 296 | CLEAR(fmt); 297 | fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 298 | fmt.fmt.pix.width = width; 299 | fmt.fmt.pix.height = height; 300 | fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; 301 | fmt.fmt.pix.colorspace = V4L2_COLORSPACE_REC709; 302 | fprintf(stderr, "[v4l2] %s: requesting image format %ux%u\n", w->name, width, height); 303 | if (-1 == _ioctl(w->fd, VIDIOC_S_FMT, &fmt)) { 304 | if(EBUSY == errno) { 305 | fprintf(stderr, "[v4l2] %s: Resource busy\n", w->name); 306 | return -1; 307 | } else { 308 | fprintf(stderr, "[v4l2] %s: Cannot set size\n", w->name); 309 | return -1; 310 | } 311 | } 312 | 313 | // Storing result 314 | w->width = fmt.fmt.pix.width; 315 | w->height = fmt.fmt.pix.height; 316 | w->colorspace = fmt.fmt.pix.colorspace; 317 | 318 | char *pixelformat = calloc(5, sizeof(char)); 319 | memcpy(pixelformat, &fmt.fmt.pix.pixelformat, 4); 320 | fprintf(stderr, "[v4l2] %s: set image format to %ux%u using %s\n", w->name, w->width, w->height, pixelformat); 321 | 322 | // Buffers have been created before, so clear them 323 | if (NULL != w->buffers) { 324 | for (i = 0; i < w->nbuffers; i++) { 325 | munmap(w->buffers[i].start, w->buffers[i].length); 326 | } 327 | 328 | free(w->buffers); 329 | } 330 | 331 | // Request the webcam's buffers for memory-mapping 332 | struct v4l2_requestbuffers req; 333 | CLEAR(req); 334 | 335 | req.count = 4; 336 | req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 337 | req.memory = V4L2_MEMORY_MMAP; 338 | 339 | if (-1 == _ioctl(w->fd, VIDIOC_REQBUFS, &req)) { 340 | if (EINVAL == errno) { 341 | fprintf(stderr, "[v4l2] %s does not support memory mapping\n", w->name); 342 | return -1; 343 | } else { 344 | fprintf(stderr, "[v4l2] Unknown error with VIDIOC_REQBUFS: %d\n", errno); 345 | return -1; 346 | } 347 | } 348 | 349 | // Needs at least 2 buffers 350 | if (req.count < 2) { 351 | fprintf(stderr, "[v4l2] Insufficient buffer memory on %s\n", w->name); 352 | return -1; 353 | } 354 | 355 | // Storing buffers in webcam structure 356 | fprintf(stderr, "[v4l2] Preparing %d buffers for %s\n", req.count, w->name); 357 | w->nbuffers = req.count; 358 | w->buffers = calloc(w->nbuffers, sizeof(struct buffer)); 359 | 360 | if (!w->buffers) { 361 | fprintf(stderr, "[v4l2] Out of memory\n"); 362 | return -1; 363 | } 364 | 365 | // Prepare buffers to be memory-mapped 366 | for (i = 0; i < w->nbuffers; ++i) { 367 | CLEAR(buf); 368 | 369 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 370 | buf.memory = V4L2_MEMORY_MMAP; 371 | buf.index = i; 372 | 373 | if (-1 == _ioctl(w->fd, VIDIOC_QUERYBUF, &buf)) { 374 | fprintf(stderr, "[v4l2] Could not query buffers on %s\n", w->name); 375 | return -1; 376 | } 377 | 378 | w->buffers[i].length = buf.length; 379 | w->buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, w->fd, buf.m.offset); 380 | 381 | if (MAP_FAILED == w->buffers[i].start) { 382 | fprintf(stderr, "[v4l2] Mmap failed\n"); 383 | return -1; 384 | } 385 | } 386 | } 387 | 388 | /** 389 | * Reads a frame from the webcam, converts it into the RGB colorspace 390 | * and stores it in the webcam structure 391 | */ 392 | static void webcam_read(struct webcam *w) 393 | { 394 | struct v4l2_buffer buf; 395 | 396 | // Try getting an image from the device 397 | for(;;) { 398 | CLEAR(buf); 399 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 400 | buf.memory = V4L2_MEMORY_MMAP; 401 | 402 | // Dequeue a (filled) buffer from the video device 403 | if (-1 == _ioctl(w->fd, VIDIOC_DQBUF, &buf)) { 404 | switch(errno) { 405 | case EAGAIN: 406 | continue; 407 | 408 | case EIO: 409 | default: 410 | fprintf(stderr, "[v4l2] %d: Could not read from device %s\n", errno, w->name); 411 | break; 412 | } 413 | } 414 | 415 | // Make sure we are not out of bounds 416 | assert(buf.index < w->nbuffers); 417 | 418 | // Lock frame mutex, and store RGB 419 | pthread_mutex_lock(&w->mtx_frame); 420 | convertToRGB(w->buffers[buf.index], &w->frame); 421 | pthread_mutex_unlock(&w->mtx_frame); 422 | break; 423 | } 424 | 425 | // Queue buffer back into the video device 426 | if (-1 == _ioctl(w->fd, VIDIOC_QBUF, &buf)) { 427 | fprintf(stderr, "[v4l2] Error while swapping buffers on %s\n", w->name); 428 | return; 429 | } 430 | } 431 | 432 | /** 433 | * The loop function for the webcam thread 434 | */ 435 | static void *webcam_streaming(void *ptr) 436 | { 437 | webcam_t *w = (webcam_t *)ptr; 438 | 439 | while(w->streaming) webcam_read(w); 440 | } 441 | 442 | /** 443 | * Tells the webcam to go into streaming mode, or to 444 | * stop streaming. 445 | * When going into streaming mode, it also creates 446 | * a thread running the webcam_streaming function. 447 | * When exiting the streaming mode, it sets the streaming 448 | * bit to false, and waits for the thread to finish. 449 | */ 450 | void webcam_stream(struct webcam *w, bool flag) 451 | { 452 | uint8_t i; 453 | 454 | struct v4l2_buffer buf; 455 | enum v4l2_buf_type type; 456 | 457 | if (flag) { 458 | // Clear buffers 459 | for (i = 0; i < w->nbuffers; i++) { 460 | CLEAR(buf); 461 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 462 | buf.memory = V4L2_MEMORY_MMAP; 463 | buf.index = i; 464 | 465 | if (-1 == _ioctl(w->fd, VIDIOC_QBUF, &buf)) { 466 | fprintf(stderr, "[v4l2] Error clearing buffers on %s\n", w->name); 467 | return; 468 | } 469 | } 470 | 471 | // Turn on streaming 472 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 473 | if (-1 == _ioctl(w->fd, VIDIOC_STREAMON, &type)) { 474 | fprintf(stderr, "[v4l2] Could not turn on streaming on %s\n", w->name); 475 | return; 476 | } 477 | 478 | // Set streaming to true and start thread 479 | w->streaming = true; 480 | pthread_create(&w->thread, NULL, webcam_streaming, (void *)w); 481 | } else { 482 | // Set streaming to false and wait for thread to finish 483 | w->streaming = false; 484 | pthread_join(w->thread, NULL); 485 | 486 | // Turn off streaming 487 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 488 | if (-1 == _ioctl(w->fd, VIDIOC_STREAMOFF, &type)) { 489 | fprintf(stderr, "[v4l2] Could not turn streaming off on %s\n", w->name); 490 | return; 491 | } 492 | } 493 | } 494 | 495 | void webcam_grab(webcam_t *w, buffer_t *frame) 496 | { 497 | // Locks the frame mutex so the grabber can copy 498 | // the frame in its own return buffer. 499 | pthread_mutex_lock(&w->mtx_frame); 500 | 501 | // Only copy frame if there is something in the webcam's frame buffer 502 | if (w->frame.length > 0) { 503 | // Initialize frame 504 | if ((*frame).start == NULL) { 505 | (*frame).start = calloc(w->frame.length, sizeof(char)); 506 | (*frame).length = w->frame.length; 507 | } 508 | 509 | memcpy((*frame).start, w->frame.start, w->frame.length); 510 | } 511 | 512 | pthread_mutex_unlock(&w->mtx_frame); 513 | } 514 | 515 | /** 516 | * Main code 517 | */ 518 | #ifdef WEBCAM_TEST 519 | int main(int argc, char **argv) 520 | { 521 | int i = 0; 522 | webcam_t *w = webcam_open("/dev/video0"); 523 | 524 | // Prepare frame, and filename, and file to store frame in 525 | buffer_t frame; 526 | frame.start = NULL; 527 | frame.length = 0; 528 | 529 | char *fn = calloc(16, sizeof(char)); 530 | FILE *fp; 531 | 532 | webcam_resize(w, 640, 480); 533 | webcam_stream(w, true); 534 | while(true) { 535 | webcam_grab(w, &frame); 536 | 537 | if (frame.length > 0) { 538 | printf("[v4l2] Storing frame %d\n", i); 539 | sprintf(fn, "frame_%d.rgb", i); 540 | fp = fopen(fn, "w+"); 541 | fwrite(frame.start, frame.length, 1, fp); 542 | fclose(fp); 543 | i++; 544 | } 545 | 546 | if (i > 10) break; 547 | } 548 | webcam_stream(w, false); 549 | webcam_close(w); 550 | 551 | if (frame.start != NULL) free(frame.start); 552 | free(fn); 553 | 554 | return 0; 555 | } 556 | #endif 557 | -------------------------------------------------------------------------------- /webcam/v4l2/webcam.go: -------------------------------------------------------------------------------- 1 | package v4l2 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // #include "webcam_wrapper.h" 8 | import "C" 9 | import "unsafe" 10 | 11 | var w *C.webcam_t 12 | 13 | func OpenWebcam(path string, width, height int) { 14 | dev := C.CString(path) 15 | defer C.free(unsafe.Pointer(dev)) 16 | w = C.go_open_webcam(dev, C.int(width), C.int(height)) 17 | // The following defer statement introduces a `double free or corruption` 18 | // error since it's already freezed in C: 19 | // 20 | // defer C.free(unsafe.Pointer(w)) 21 | 22 | // Now open the device 23 | log.Println("Webcam opened") 24 | } 25 | 26 | func GrabFrame() []byte { 27 | buf := C.go_grab_frame(w) 28 | result := C.GoBytes(unsafe.Pointer(buf.start), C.int(buf.length)) 29 | // Free the buffer (better way for this?) 30 | if unsafe.Pointer(buf.start) != unsafe.Pointer(uintptr(0)) { 31 | C.free(unsafe.Pointer(buf.start)) 32 | } 33 | return result 34 | } 35 | 36 | func CloseWebcam() { 37 | if C.go_close_webcam(w) == 0 { 38 | log.Println("Webcam closed") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webcam/v4l2/webcam.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #define CLEAR(x) memset(&(x), 0, sizeof(x)) 19 | 20 | /** 21 | * Buffer structure 22 | */ 23 | typedef struct buffer { 24 | uint8_t *start; 25 | size_t length; 26 | } buffer_t; 27 | 28 | /** 29 | * Webcam structure 30 | */ 31 | typedef struct webcam { 32 | char *name; 33 | int fd; 34 | buffer_t *buffers; 35 | uint8_t nbuffers; 36 | 37 | buffer_t frame; 38 | pthread_t thread; 39 | pthread_mutex_t mtx_frame; 40 | 41 | uint16_t width; 42 | uint16_t height; 43 | uint8_t colorspace; 44 | 45 | char formats[16][5]; 46 | bool streaming; 47 | } webcam_t; 48 | 49 | webcam_t *webcam_open(const char *dev); 50 | void webcam_close(webcam_t *w); 51 | int webcam_resize(webcam_t *w, uint16_t width, uint16_t height); 52 | void webcam_stream(webcam_t *w, bool flag); 53 | void webcam_grab(webcam_t *w, buffer_t *frame); 54 | -------------------------------------------------------------------------------- /webcam/v4l2/webcam_wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef WEBCAM_WRAPPER_H 2 | #define WEBCAM_WRAPPER_H 3 | 4 | #include "webcam.h" 5 | 6 | webcam_t* go_open_webcam(const char* dev, int width, int height) { 7 | webcam_t *w = webcam_open(dev); 8 | if (w == NULL) { 9 | fprintf(stderr, "[v4l2] Failed to open the webcam.\n"); 10 | exit(EXIT_FAILURE); 11 | } 12 | 13 | if (-1 == webcam_resize(w, width, height)) { 14 | fprintf(stderr, "[v4l2] Failed to resize the webcam\n"); 15 | exit(EXIT_FAILURE); 16 | } 17 | 18 | webcam_stream(w, true); 19 | return w; 20 | } 21 | 22 | buffer_t go_grab_frame(webcam_t* w) { 23 | buffer_t frame = { NULL, 0 }; 24 | while(frame.length==0) { 25 | webcam_grab(w, &frame); 26 | } 27 | return frame; 28 | } 29 | 30 | int go_close_webcam(webcam_t* w) { 31 | webcam_stream(w, false); 32 | webcam_close(w); 33 | return 0; 34 | } 35 | 36 | #endif 37 | --------------------------------------------------------------------------------