feat: use MJPEG for live capture
This commit is contained in:
parent
dd16a82057
commit
408c9c6d89
9 changed files with 112 additions and 151 deletions
|
|
@ -102,7 +102,5 @@ remote:
|
|||
label: Volume
|
||||
- label: Desktop
|
||||
items:
|
||||
- type: screenshot
|
||||
label: Screenshot
|
||||
- type: live_video
|
||||
label: Live video
|
||||
- type: capture
|
||||
label: Capture
|
||||
|
|
|
|||
67
live_controller.go
Normal file
67
live_controller.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kbinani/screenshot"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func captureController(c echo.Context) error {
|
||||
bounds := screenshot.GetDisplayBounds(0)
|
||||
|
||||
switch c.QueryParam("type") {
|
||||
case "screenshot":
|
||||
if img, err := screenshot.CaptureRect(bounds); err == nil {
|
||||
var buf bytes.Buffer
|
||||
jpeg.Encode(&buf, img, nil)
|
||||
|
||||
c.Response().Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
return c.Blob(http.StatusOK, "image/jpeg", buf.Bytes())
|
||||
}
|
||||
default:
|
||||
c.Response().Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
for {
|
||||
if img, err := screenshot.CaptureRect(bounds); err == nil {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if c.QueryParam("pointer") == "1" {
|
||||
currentX, currentY := GetPointerPosition()
|
||||
pointerSize := 2 * 16.0
|
||||
|
||||
pixelColor := color.RGBA{
|
||||
R: 255,
|
||||
G: 0,
|
||||
B: 0,
|
||||
A: 255,
|
||||
}
|
||||
|
||||
for x := math.Max(0.0, currentX-pointerSize/2); x <= currentX+3; x++ {
|
||||
for y := math.Max(0.0, currentY-pointerSize/2); y < currentY+3; y++ {
|
||||
img.SetRGBA(int(x), int(y), pixelColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jpeg.Encode(&buf, img, nil)
|
||||
|
||||
_, _ = c.Response().Write([]byte("--frame\r\n"))
|
||||
|
||||
c.Response().Write([]byte("Content-Type: image/jpeg\r\n\r\n"))
|
||||
c.Response().Write(buf.Bytes())
|
||||
c.Response().Write([]byte("\r\n"))
|
||||
|
||||
time.Sleep(33 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1
main.go
1
main.go
|
|
@ -63,6 +63,7 @@ func main() {
|
|||
e.GET("/manifest.webmanifest", manifestController)
|
||||
e.GET("/", homeController)
|
||||
e.GET("/ws", wsController)
|
||||
e.GET("/capture", captureController)
|
||||
|
||||
if config.Server.Tls.Enable == false {
|
||||
e.Logger.Fatal(e.Start(config.Server.Listen))
|
||||
|
|
|
|||
BIN
remote-i3wm-go
Executable file
BIN
remote-i3wm-go
Executable file
Binary file not shown.
12
rice-box.go
12
rice-box.go
File diff suppressed because one or more lines are too long
|
|
@ -157,7 +157,7 @@ a {
|
|||
display: none;
|
||||
}
|
||||
|
||||
#screenshot img {
|
||||
.capture-img img {
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ let wsLock = false
|
|||
let isPointerLive = false
|
||||
let isScreenshotWaiting = null
|
||||
let isPointerScreenshotWaiting = false
|
||||
let emptyImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"
|
||||
|
||||
function createWebSocketConnection() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
|
|
@ -24,7 +25,7 @@ function createWebSocketConnection() {
|
|||
|
||||
window.setTimeout(createWebSocketConnection, 5000)
|
||||
})
|
||||
|
||||
|
||||
ws.addEventListener('message', function(event) {
|
||||
unLock()
|
||||
let data = JSON.parse(event.data)
|
||||
|
|
@ -47,27 +48,6 @@ function createWebSocketConnection() {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'screenshot') {
|
||||
if (isScreenshotWaiting || isScreenshotWaiting === null) {
|
||||
if (isScreenshotWaiting) {
|
||||
isScreenshotWaiting = false
|
||||
}
|
||||
|
||||
screenshotImg.setAttribute('src', 'data:image/png;base64, ' + data.value)
|
||||
}
|
||||
|
||||
let pointer = document.querySelector('#pointer')
|
||||
|
||||
if (isPointerScreenshotWaiting) {
|
||||
pointer.style.backgroundImage = `url('data:image/png;base64, ${data.value}')`
|
||||
isPointerScreenshotWaiting = false
|
||||
} else {
|
||||
pointer.style.backgroundImage = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -223,26 +203,10 @@ function pointerTouchStartHandler(e) {
|
|||
|
||||
function pointerLiveHandler(e) {
|
||||
if (!e.target.checked) {
|
||||
isPointerLive = false
|
||||
isPointerScreenshotWaiting = null
|
||||
|
||||
return
|
||||
pointer.style.backgroundImage = ""
|
||||
} else {
|
||||
pointer.style.backgroundImage = `url("/capture?type=live&pointer=1&${Math.random()}")`
|
||||
}
|
||||
|
||||
isPointerLive = true
|
||||
|
||||
let doScreenshot = function() {
|
||||
if (isPointerLive) {
|
||||
if (!isPointerScreenshotWaiting) {
|
||||
isPointerScreenshotWaiting = true
|
||||
ws.send(`{"type":"screenshot","quality":"lq","pointer":true}`)
|
||||
}
|
||||
|
||||
window.setTimeout(doScreenshot, 300)
|
||||
}
|
||||
}
|
||||
|
||||
doScreenshot()
|
||||
}
|
||||
|
||||
function pointerTouchMoveHandler(e) {
|
||||
|
|
@ -265,46 +229,30 @@ function pointerTouchMoveHandler(e) {
|
|||
ws.send(msg)
|
||||
}
|
||||
|
||||
function liveHqClickHandler(e) {
|
||||
return liveClickHandler(e, 'hq')
|
||||
function capture(mode) {
|
||||
}
|
||||
|
||||
function liveLqClickHandler(e) {
|
||||
return liveClickHandler(e, 'lq')
|
||||
function captureScreenshotClickHandler(e) {
|
||||
const img = e.target.parentNode.querySelector('.capture-img img')
|
||||
img.src = "/capture?type=screenshot&" + Math.random()
|
||||
}
|
||||
|
||||
function liveClickHandler(e, quality) {
|
||||
if (isLive) {
|
||||
isLive = false
|
||||
isScreenshotWaiting = null
|
||||
function captureLiveClickHandler(e) {
|
||||
const img = e.target.parentNode.querySelector('.capture-img img')
|
||||
|
||||
document.querySelector('#live-hq').innerText = 'Live HQ'
|
||||
document.querySelector('#live-lq').innerText = 'Live LQ'
|
||||
|
||||
return
|
||||
if (img.src.indexOf("live") > -1) {
|
||||
img.src = emptyImg
|
||||
} else {
|
||||
img.src = "/capture?type=live&" + Math.random()
|
||||
}
|
||||
|
||||
isLive = true
|
||||
|
||||
e.target.innerText = 'Stop live'
|
||||
|
||||
let doScreenshot = function() {
|
||||
if (isLive) {
|
||||
if (!isScreenshotWaiting) {
|
||||
isScreenshotWaiting = true
|
||||
ws.send(`{"type":"screenshot","quality":"${quality}"}`)
|
||||
}
|
||||
|
||||
window.setTimeout(doScreenshot, 100)
|
||||
}
|
||||
}
|
||||
|
||||
doScreenshot()
|
||||
}
|
||||
|
||||
function fullscreenHandler(e) {
|
||||
let element = document.querySelector(e.target.getAttribute('data-target'))
|
||||
let isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
|
||||
const targetConf = e.target.getAttribute('data-target')
|
||||
const isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
|
||||
const element = (targetConf === 'this')
|
||||
? e.target
|
||||
: document.querySelector(targetConf)
|
||||
|
||||
document.querySelector('body').classList.toggle('fullscreen', isFullscreen)
|
||||
|
||||
|
|
@ -372,8 +320,9 @@ function addListeners() {
|
|||
addEventListenerOn(pointer, 'touchmove', pointerTouchMoveHandler)
|
||||
addEventListenerOn('#mouse-screenshot-live input', 'change', pointerLiveHandler)
|
||||
|
||||
addEventListenerOn('#live-hq', 'click', liveHqClickHandler)
|
||||
addEventListenerOn('#live-lq', 'click', liveLqClickHandler)
|
||||
addEventListenerOn('.capture-live', 'click', captureLiveClickHandler)
|
||||
addEventListenerOn('.capture-screenshot', 'click', captureScreenshotClickHandler)
|
||||
|
||||
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,18 +124,17 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if eq $value.Type "screenshot"}}
|
||||
{{if eq $value.Type "capture"}}
|
||||
<div class="col-12">
|
||||
<button type="button" data-msg='{"type":"screenshot","quality":"hq"}' class="btn btn-secondary">Screenshot HQ</button>
|
||||
<button type="button" data-msg='{"type":"screenshot","quality":"lq"}' class="btn btn-secondary">Screenshot LQ</button>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if eq $value.Type "live_video"}}
|
||||
<div class="col-12">
|
||||
<button type="button" id="live-hq" class="btn btn-secondary">Live HQ</button>
|
||||
<button type="button" id="live-lq" class="btn btn-secondary">Live LQ</button>
|
||||
<div id="screenshot"><img class="btn-fullscreen" data-target="#screenshot img" src="data:image/png; base64, iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"></div>
|
||||
<button type="button" class="btn btn-secondary capture-screenshot">
|
||||
Screenshot
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary capture-live">
|
||||
Live
|
||||
</button>
|
||||
<div class="capture-img">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC" class="btn-fullscreen" data-target="this">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"math"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
|
@ -15,7 +11,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kbinani/screenshot"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
|
|
@ -61,7 +56,7 @@ func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
|
|||
ws.WriteMessage(websocket.TextMessage, value)
|
||||
}
|
||||
|
||||
func getPointerPosition() (float64, float64) {
|
||||
func GetPointerPosition() (float64, float64) {
|
||||
location := exec.Command("xdotool", "getmouselocation")
|
||||
output, _ := location.Output()
|
||||
position := string(output)
|
||||
|
|
@ -106,7 +101,7 @@ func createActions() Actions {
|
|||
return cmd.Run()
|
||||
}
|
||||
|
||||
currentX, currentY := getPointerPosition()
|
||||
currentX, currentY := GetPointerPosition()
|
||||
|
||||
newX, _ := strconv.ParseFloat(data.X, 32)
|
||||
newY, _ := strconv.ParseFloat(data.Y, 32)
|
||||
|
|
@ -335,54 +330,6 @@ func createActions() Actions {
|
|||
return cmd.Run()
|
||||
})
|
||||
|
||||
actions.add("screenshot", func(ws *websocket.Conn, msg []byte) error {
|
||||
data := ScreenshotMessageData{}
|
||||
json.Unmarshal([]byte(msg), &data)
|
||||
|
||||
bounds := screenshot.GetDisplayBounds(0)
|
||||
img, err := screenshot.CaptureRect(bounds)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("Capture error")
|
||||
}
|
||||
|
||||
var quality int
|
||||
|
||||
if data.Quality == "lq" {
|
||||
quality = 10
|
||||
} else {
|
||||
quality = 90
|
||||
}
|
||||
|
||||
if data.Pointer {
|
||||
currentX, currentY := getPointerPosition()
|
||||
pointerSize := 2 * 16.0
|
||||
|
||||
pixelColor := color.RGBA{
|
||||
R: 255,
|
||||
G: 0,
|
||||
B: 0,
|
||||
A: 255,
|
||||
}
|
||||
|
||||
for x := math.Max(0.0, currentX-pointerSize/2); x <= currentX+3; x++ {
|
||||
for y := math.Max(0.0, currentY-pointerSize/2); y < currentY+3; y++ {
|
||||
img.SetRGBA(int(x), int(y), pixelColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
jpeg.Encode(buff, img, &jpeg.Options{Quality: quality})
|
||||
|
||||
sendMessageResponse(ws, MessageResponse{
|
||||
Type: "screenshot",
|
||||
Value: toBase64(buff.Bytes()),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
actions.add("messages", func(ws *websocket.Conn, msg []byte) error {
|
||||
data := MessagesData{}
|
||||
json.Unmarshal([]byte(msg), &data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue