require ( | require ( | ||||
github.com/BurntSushi/toml v0.4.1 | github.com/BurntSushi/toml v0.4.1 | ||||
github.com/go-vgo/robotgo v0.99.0 | github.com/go-vgo/robotgo v0.99.0 | ||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 | |||||
) | ) |
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY= | |||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo= | |||||
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= | ||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= | ||||
github.com/go-vgo/robotgo v0.99.0 h1:Lp6kvic1/LxkSShf6KNjcrcE21FCFlonrCguQN96Ay4= | github.com/go-vgo/robotgo v0.99.0 h1:Lp6kvic1/LxkSShf6KNjcrcE21FCFlonrCguQN96Ay4= | ||||
github.com/go-vgo/robotgo v0.99.0/go.mod h1:0+i2QWRmZtbIF02RwmiGfFj33Judprukd8ls5J6Eajg= | github.com/go-vgo/robotgo v0.99.0/go.mod h1:0+i2QWRmZtbIF02RwmiGfFj33Judprukd8ls5J6Eajg= | ||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4= | |||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4= | |||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 h1:qq2nCpSrXrmvDGRxW0ruW9BVEV1CN2a9YDOExdt+U0o= | |||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4= | |||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= | github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= | ||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= | ||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= |
import "errors" | import "errors" | ||||
import "log" | import "log" | ||||
import "os" | import "os" | ||||
import "fmt" | |||||
import "path" | import "path" | ||||
import "net/http" | import "net/http" | ||||
import "io/ioutil" | import "io/ioutil" | ||||
import "encoding/json" | import "encoding/json" | ||||
import "github.com/go-vgo/robotgo" | import "github.com/go-vgo/robotgo" | ||||
import "github.com/BurntSushi/toml" | import "github.com/BurntSushi/toml" | ||||
import "coffee.mort.mediator/screencap" | |||||
type Config struct { | type Config struct { | ||||
BasePath string `toml:"base_path"` | BasePath string `toml:"base_path"` | ||||
fs := http.FileServer(http.Dir("./web")) | fs := http.FileServer(http.Dir("./web")) | ||||
http.Handle("/", fs) | http.Handle("/", fs) | ||||
http.HandleFunc("/api/screen-size", handler(func(w RW, req *Req) error { | |||||
http.HandleFunc("/api/remote/screen-size", handler(func(w RW, req *Req) error { | |||||
if req.Method == "GET" { | if req.Method == "GET" { | ||||
var size ScreenSizeData | var size ScreenSizeData | ||||
size.Width, size.Height = robotgo.GetScreenSize() | size.Width, size.Height = robotgo.GetScreenSize() | ||||
} | } | ||||
})) | })) | ||||
http.HandleFunc("/api/mouse-pos", handler(func(w RW, req *Req) error { | |||||
http.HandleFunc("/api/remote/mouse-pos", handler(func(w RW, req *Req) error { | |||||
if req.Method == "GET" { | if req.Method == "GET" { | ||||
var pos MousePosData | var pos MousePosData | ||||
pos.X, pos.Y = robotgo.GetMousePos() | pos.X, pos.Y = robotgo.GetMousePos() | ||||
} | } | ||||
})) | })) | ||||
http.HandleFunc("/api/remote/screencast", handler(func(w RW, req *Req) error { | |||||
if req.Method == "GET" { | |||||
w.Header().Add("Content-Type", "multipart/x-mixed-replace;boundary=MEDIATOR_FRAME_BOUNDARY") | |||||
w.WriteHeader(200) | |||||
for { | |||||
img := <-screencap.Capture() | |||||
log.Printf("Got image, %v bytes", img.Length) | |||||
var err error | |||||
_, err = w.Write([]byte(fmt.Sprintf( | |||||
"--MEDIATOR_FRAME_BOUNDARY\r\n" + | |||||
"Content-Type: image/jpeg\r\n" + | |||||
"Content-Length: %d\r\n" + | |||||
"\r\n", img.Length))) | |||||
if err != nil { | |||||
log.Printf("Write error: %v", err) | |||||
return nil | |||||
} | |||||
_, err = w.Write(img.Data[0:img.Length]) | |||||
if err != nil { | |||||
log.Printf("Write error: %v", err) | |||||
return nil | |||||
} | |||||
_, err = w.Write([]byte("\r\n")) | |||||
if err != nil { | |||||
log.Printf("Write error: %v", err) | |||||
return nil | |||||
} | |||||
} | |||||
} else { | |||||
return errors.New("Invalid method: " + req.Method) | |||||
} | |||||
})); | |||||
http.HandleFunc("/api/dir/", handler(func(w RW, req *Req) error { | http.HandleFunc("/api/dir/", handler(func(w RW, req *Req) error { | ||||
if req.Method == "GET" { | if req.Method == "GET" { | ||||
subPath := req.URL.Path[len("/api/dir/"):] | subPath := req.URL.Path[len("/api/dir/"):] | ||||
} | } | ||||
})) | })) | ||||
go screencap.Run() | |||||
log.Println("Listening on :3000...") | log.Println("Listening on :3000...") | ||||
err = http.ListenAndServe("localhost:3000", nil) | err = http.ListenAndServe("localhost:3000", nil) | ||||
if err != nil { | if err != nil { |
package screencap | |||||
import "github.com/kbinani/screenshot" | |||||
import "image/jpeg" | |||||
import "sync" | |||||
import "time" | |||||
import "log" | |||||
type Buffer struct { | |||||
Data []byte | |||||
Length int | |||||
} | |||||
func (buf *Buffer) Write(data []byte) (int, error) { | |||||
if len(buf.Data) == 0 { | |||||
l := len(data) | |||||
if l < 1024 { | |||||
l = 2024 | |||||
} | |||||
buf.Data = make([]byte, l) | |||||
} else if buf.Length + len(data) > len(buf.Data) { | |||||
newSize := len(buf.Data) * 2 | |||||
for buf.Length + len(data) > newSize { | |||||
newSize *= 2 | |||||
} | |||||
newBuf := make([]byte, newSize) | |||||
copy(newBuf, buf.Data[0:buf.Length]) | |||||
buf.Data = newBuf | |||||
} | |||||
copy(buf.Data[buf.Length:], data) | |||||
buf.Length += len(data) | |||||
return len(data), nil | |||||
} | |||||
var ( | |||||
mut = sync.Mutex{} | |||||
chans = make([]chan *Buffer, 0) | |||||
startChan = make(chan struct{}, 1) | |||||
buffers = make([]Buffer, 4) | |||||
currentBuffer = 0 | |||||
) | |||||
func Capture() chan *Buffer { | |||||
ch := make(chan *Buffer) | |||||
mut.Lock() | |||||
chans = append(chans, ch) | |||||
mut.Unlock() | |||||
select { | |||||
case startChan <- struct{}{}: | |||||
default: | |||||
} | |||||
return ch | |||||
} | |||||
func Run() { | |||||
for { | |||||
<-startChan | |||||
img, err := screenshot.CaptureDisplay(0) | |||||
if err != nil { | |||||
log.Printf("Failed to capture screenshot: %v", err) | |||||
time.Sleep(2 * time.Second) | |||||
continue | |||||
} | |||||
buf := &buffers[currentBuffer] | |||||
currentBuffer = (currentBuffer + 1) % len(buffers) | |||||
buf.Length = 0 | |||||
err = jpeg.Encode(buf, img, &jpeg.Options{Quality: 80}) | |||||
if err != nil { | |||||
log.Printf("Failed to encode jpeg: %v", err) | |||||
time.Sleep(2 * time.Second) | |||||
continue | |||||
} | |||||
mut.Lock() | |||||
for _, ch := range chans { | |||||
ch <- buf | |||||
} | |||||
chans = make([]chan *Buffer, 0) | |||||
mut.Unlock() | |||||
} | |||||
} |
<html> | <html> | ||||
<head> | <head> | ||||
<meta charset="utf-8"> | <meta charset="utf-8"> | ||||
<title>Mediator</title> | |||||
<title>Mediator - Files</title> | |||||
<link rel="stylesheet" href="style.css"> | <link rel="stylesheet" href="style.css"> | ||||
<link rel="icon" type="image/png" href="favicon.png"> | <link rel="icon" type="image/png" href="favicon.png"> | ||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<title>Mediator - Remote</title> | |||||
<link rel="stylesheet" href="style.css"> | |||||
<link rel="icon" type="image/png" href="favicon.png"> | |||||
</head> | |||||
<body> | |||||
<img src="/api/remote/screencast"> | |||||
<script src="util.js"></script> | |||||
<script> | |||||
async function main() { | |||||
let screenSize = api("GET", "remote/screen-size"); | |||||
let mousePos = api("GET", "remote/mouse-pos"); | |||||
} | |||||
main(); | |||||
</script> | |||||
</body> | |||||
</html> |