| @@ -5,4 +5,5 @@ go 1.16 | |||
| require ( | |||
| github.com/BurntSushi/toml v0.4.1 | |||
| github.com/go-vgo/robotgo v0.99.0 | |||
| github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 | |||
| ) | |||
| @@ -7,10 +7,16 @@ github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrU | |||
| 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/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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= | |||
| 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/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/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= | |||
| github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= | |||
| @@ -3,12 +3,14 @@ package main | |||
| import "errors" | |||
| import "log" | |||
| import "os" | |||
| import "fmt" | |||
| import "path" | |||
| import "net/http" | |||
| import "io/ioutil" | |||
| import "encoding/json" | |||
| import "github.com/go-vgo/robotgo" | |||
| import "github.com/BurntSushi/toml" | |||
| import "coffee.mort.mediator/screencap" | |||
| type Config struct { | |||
| BasePath string `toml:"base_path"` | |||
| @@ -81,7 +83,7 @@ func main() { | |||
| fs := http.FileServer(http.Dir("./web")) | |||
| 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" { | |||
| var size ScreenSizeData | |||
| size.Width, size.Height = robotgo.GetScreenSize() | |||
| @@ -91,7 +93,7 @@ func main() { | |||
| } | |||
| })) | |||
| 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" { | |||
| var pos MousePosData | |||
| pos.X, pos.Y = robotgo.GetMousePos() | |||
| @@ -110,6 +112,43 @@ func main() { | |||
| } | |||
| })) | |||
| 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 { | |||
| if req.Method == "GET" { | |||
| subPath := req.URL.Path[len("/api/dir/"):] | |||
| @@ -140,6 +179,8 @@ func main() { | |||
| } | |||
| })) | |||
| go screencap.Run() | |||
| log.Println("Listening on :3000...") | |||
| err = http.ListenAndServe("localhost:3000", nil) | |||
| if err != nil { | |||
| @@ -0,0 +1,88 @@ | |||
| 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() | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <title>Mediator</title> | |||
| <title>Mediator - Files</title> | |||
| <link rel="stylesheet" href="style.css"> | |||
| <link rel="icon" type="image/png" href="favicon.png"> | |||
| @@ -0,0 +1,23 @@ | |||
| <!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> | |||