inital commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
//go:build linux
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
var activeDisplays = func() int { return 0 }
|
||||
|
||||
var captureDisplayFn = func(int) (*image.RGBA, error) { return nil, nil }
|
||||
|
||||
func displayCount() int { return 0 }
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import "image"
|
||||
|
||||
func CaptureDisplayRGBABitBlt(display int) (*image.RGBA, error) {
|
||||
return captureDisplayBitBlt(display)
|
||||
}
|
||||
|
||||
func CaptureDisplayRGBAPreferBitBlt(display int) (*image.RGBA, error) {
|
||||
img, err := captureDisplayBitBlt(display)
|
||||
if err == nil && img != nil {
|
||||
return img, nil
|
||||
}
|
||||
return CaptureDisplayRGBA(display)
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"overlord-client/cmd/agent/config"
|
||||
rt "overlord-client/cmd/agent/runtime"
|
||||
"overlord-client/cmd/agent/wire"
|
||||
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
type recordingWriter struct {
|
||||
msgs [][]byte
|
||||
types []websocket.MessageType
|
||||
}
|
||||
|
||||
func (w *recordingWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
|
||||
w.types = append(w.types, messageType)
|
||||
w.msgs = append(w.msgs, append([]byte(nil), p...))
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCaptureAndSend_NoDisplays(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
activeDisplays = func() int { return 0 }
|
||||
t.Cleanup(func() { activeDisplays = originalCount })
|
||||
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
||||
if err := CaptureAndSend(context.Background(), env); err != nil {
|
||||
t.Fatalf("expected nil error when no displays, got %v", err)
|
||||
}
|
||||
if len(writer.msgs) != 0 {
|
||||
t.Fatalf("expected no messages when no displays, got %d", len(writer.msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureAndSend_SendsFrame(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
originalCapture := captureDisplayFn
|
||||
activeDisplays = func() int { return 1 }
|
||||
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||
|
||||
img.Set(0, 0, color.RGBA{R: 200, G: 10, B: 10, A: 255})
|
||||
return img, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
activeDisplays = originalCount
|
||||
captureDisplayFn = originalCapture
|
||||
})
|
||||
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
||||
if err := CaptureAndSend(context.Background(), env); err != nil {
|
||||
t.Fatalf("CaptureAndSend returned error: %v", err)
|
||||
}
|
||||
if len(writer.msgs) != 1 {
|
||||
t.Fatalf("expected one message written, got %d", len(writer.msgs))
|
||||
}
|
||||
|
||||
var frame wire.Frame
|
||||
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
||||
t.Fatalf("decode frame: %v", err)
|
||||
}
|
||||
if frame.Type != "frame" {
|
||||
t.Fatalf("unexpected frame type: %s", frame.Type)
|
||||
}
|
||||
if frame.Header.Format != "jpeg" {
|
||||
t.Fatalf("unexpected frame format: %s", frame.Header.Format)
|
||||
}
|
||||
if len(frame.Data) == 0 {
|
||||
t.Fatalf("expected jpeg data to be present")
|
||||
}
|
||||
|
||||
if _, err := jpeg.Decode(bytes.NewReader(frame.Data)); err != nil {
|
||||
t.Fatalf("jpeg payload did not decode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActualScreenCapture(t *testing.T) {
|
||||
displayCount := safeDisplayCount()
|
||||
if displayCount == 0 {
|
||||
t.Log("WARNING: No displays detected, skipping real screen capture test")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Testing screen capture with %d display(s)", displayCount)
|
||||
|
||||
img, err := safeCaptureDisplay(0)
|
||||
if err != nil {
|
||||
t.Logf("WARNING: Failed to capture display 0: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
t.Log("WARNING: Capture returned nil image")
|
||||
return
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
||||
t.Errorf("Invalid image dimensions: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Successfully captured %dx%d image from display 0", bounds.Dx(), bounds.Dy())
|
||||
|
||||
hasVariation := false
|
||||
firstPixel := img.RGBAAt(0, 0)
|
||||
for y := 0; y < bounds.Dy() && !hasVariation; y++ {
|
||||
for x := 0; x < bounds.Dx(); x++ {
|
||||
pixel := img.RGBAAt(x, y)
|
||||
if pixel != firstPixel {
|
||||
hasVariation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasVariation {
|
||||
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
|
||||
}
|
||||
|
||||
quality := jpegQuality()
|
||||
frame, encodeDur, err := buildFrame(img, 0, quality)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to build frame from captured image: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Encoded frame: size=%d bytes, encode_time=%s, format=%s",
|
||||
len(frame.Data), encodeDur, frame.Header.Format)
|
||||
|
||||
decodedImg, err := jpeg.Decode(bytes.NewReader(frame.Data))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode JPEG frame: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if decodedImg.Bounds().Dx() <= 0 || decodedImg.Bounds().Dy() <= 0 {
|
||||
t.Errorf("Decoded image has invalid dimensions: %dx%d",
|
||||
decodedImg.Bounds().Dx(), decodedImg.Bounds().Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleDisplays(t *testing.T) {
|
||||
displayCount := safeDisplayCount()
|
||||
if displayCount == 0 {
|
||||
t.Log("WARNING: No displays detected, skipping multi-display test")
|
||||
return
|
||||
}
|
||||
|
||||
if displayCount == 1 {
|
||||
t.Log("Only one display detected, testing single display")
|
||||
}
|
||||
|
||||
for i := 0; i < displayCount; i++ {
|
||||
t.Run("Display"+string(rune('0'+i)), func(t *testing.T) {
|
||||
img, err := safeCaptureDisplay(i)
|
||||
if err != nil {
|
||||
t.Logf("WARNING: Failed to capture display %d: %v", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
t.Logf("WARNING: Capture returned nil for display %d", i)
|
||||
return
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
t.Logf("Display %d: %dx%d", i, bounds.Dx(), bounds.Dy())
|
||||
|
||||
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
||||
t.Errorf("Display %d has invalid dimensions", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsCapture(t *testing.T) {
|
||||
hasDisplays := supportsCapture()
|
||||
displayCount := safeDisplayCount()
|
||||
|
||||
if hasDisplays && displayCount == 0 {
|
||||
t.Error("supportsCapture returned true but displayCount is 0")
|
||||
}
|
||||
|
||||
if !hasDisplays && displayCount > 0 {
|
||||
t.Error("supportsCapture returned false but displayCount > 0")
|
||||
}
|
||||
|
||||
t.Logf("Capture support: %v (displays: %d)", hasDisplays, displayCount)
|
||||
}
|
||||
|
||||
func TestCaptureWithDifferentDisplayIndices(t *testing.T) {
|
||||
displayCount := safeDisplayCount()
|
||||
if displayCount == 0 {
|
||||
t.Skip("No displays available")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
index int
|
||||
valid bool
|
||||
}{
|
||||
{"ValidFirst", 0, true},
|
||||
{"ValidLast", displayCount - 1, true},
|
||||
{"InvalidNegative", -1, false},
|
||||
{"InvalidTooHigh", displayCount + 10, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
img, err := safeCaptureDisplay(tt.index)
|
||||
|
||||
if tt.valid {
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid capture, got error: %v", err)
|
||||
}
|
||||
if img == nil {
|
||||
t.Error("Expected image, got nil")
|
||||
}
|
||||
} else {
|
||||
|
||||
if img != nil && err == nil {
|
||||
t.Logf("WARNING: Invalid index %d returned image instead of error", tt.index)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameFPS(t *testing.T) {
|
||||
|
||||
resetStats()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
statFrames.Add(1)
|
||||
}
|
||||
|
||||
fps := frameFPS(time.Now())
|
||||
if fps < 0 {
|
||||
t.Errorf("FPS should not be negative, got %d", fps)
|
||||
}
|
||||
|
||||
t.Logf("Calculated FPS: %d", fps)
|
||||
}
|
||||
|
||||
func TestJPEGQuality(t *testing.T) {
|
||||
quality := jpegQuality()
|
||||
if quality < 1 || quality > 100 {
|
||||
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
|
||||
}
|
||||
t.Logf("JPEG quality: %d", quality)
|
||||
}
|
||||
|
||||
func TestCaptureAndSend_DisplayOutOfRange(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
originalCapture := captureDisplayFn
|
||||
activeDisplays = func() int { return 2 }
|
||||
capturedDisplay := -1
|
||||
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
||||
capturedDisplay = displayIndex
|
||||
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||
return img, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
activeDisplays = originalCount
|
||||
captureDisplayFn = originalCapture
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selectedDisplay int
|
||||
expectedDisplay int
|
||||
}{
|
||||
{"NegativeDisplay", -1, 0},
|
||||
{"TooHighDisplay", 10, 0},
|
||||
{"ValidDisplay", 1, 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
capturedDisplay = -1
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{
|
||||
Conn: writer,
|
||||
Cfg: config.Config{},
|
||||
SelectedDisplay: tt.selectedDisplay,
|
||||
}
|
||||
|
||||
if err := CaptureAndSend(context.Background(), env); err != nil {
|
||||
t.Fatalf("CaptureAndSend failed: %v", err)
|
||||
}
|
||||
|
||||
if capturedDisplay != tt.expectedDisplay {
|
||||
t.Errorf("expected capture from display %d, got %d", tt.expectedDisplay, capturedDisplay)
|
||||
}
|
||||
|
||||
if len(writer.msgs) != 1 {
|
||||
t.Errorf("expected 1 message, got %d", len(writer.msgs))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNow_DisableCapture(t *testing.T) {
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{
|
||||
Conn: writer,
|
||||
Cfg: config.Config{DisableCapture: true},
|
||||
}
|
||||
|
||||
if err := Now(context.Background(), env); err != nil {
|
||||
t.Fatalf("Now with DisableCapture failed: %v", err)
|
||||
}
|
||||
|
||||
if len(writer.msgs) != 1 {
|
||||
t.Fatalf("expected 1 black frame message, got %d", len(writer.msgs))
|
||||
}
|
||||
|
||||
var frame wire.Frame
|
||||
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
||||
t.Fatalf("failed to decode frame: %v", err)
|
||||
}
|
||||
|
||||
if frame.Type != "frame" {
|
||||
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
|
||||
}
|
||||
|
||||
img, err := jpeg.Decode(bytes.NewReader(frame.Data))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode JPEG: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() != 64 || bounds.Dy() != 64 {
|
||||
t.Errorf("expected 64x64 black frame, got %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNow_NoDisplays(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
activeDisplays = func() int { return 0 }
|
||||
t.Cleanup(func() { activeDisplays = originalCount })
|
||||
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
||||
|
||||
if err := Now(context.Background(), env); err != nil {
|
||||
t.Fatalf("Now with no displays failed: %v", err)
|
||||
}
|
||||
|
||||
if len(writer.msgs) != 0 {
|
||||
t.Errorf("expected no messages when no displays, got %d", len(writer.msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNow_Success(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
originalCapture := captureDisplayFn
|
||||
activeDisplays = func() int { return 1 }
|
||||
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 4, 4))
|
||||
return img, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
activeDisplays = originalCount
|
||||
captureDisplayFn = originalCapture
|
||||
})
|
||||
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
||||
|
||||
if err := Now(context.Background(), env); err != nil {
|
||||
t.Fatalf("Now failed: %v", err)
|
||||
}
|
||||
|
||||
if len(writer.msgs) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(writer.msgs))
|
||||
}
|
||||
|
||||
var frame wire.Frame
|
||||
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
||||
t.Fatalf("failed to decode frame: %v", err)
|
||||
}
|
||||
|
||||
if frame.Type != "frame" {
|
||||
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorCount(t *testing.T) {
|
||||
count := MonitorCount()
|
||||
|
||||
// no monitor means weird
|
||||
if count < 1 {
|
||||
t.Errorf("expected at least 1, got %d", count)
|
||||
}
|
||||
|
||||
t.Logf("MonitorCount returned: %d", count)
|
||||
}
|
||||
|
||||
func TestCaptureAndSend_CancelledContext(t *testing.T) {
|
||||
originalCount := activeDisplays
|
||||
originalCapture := captureDisplayFn
|
||||
activeDisplays = func() int { return 1 }
|
||||
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||
return img, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
activeDisplays = originalCount
|
||||
captureDisplayFn = originalCapture
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // fuck no
|
||||
|
||||
writer := &recordingWriter{}
|
||||
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
||||
|
||||
// SHOULD handle gracefully, may or may not send depending on timing :sob:
|
||||
_ = CaptureAndSend(ctx, env)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build !linux && !windows
|
||||
|
||||
package capture
|
||||
|
||||
import "github.com/kbinani/screenshot"
|
||||
|
||||
var (
|
||||
activeDisplays = screenshot.NumActiveDisplays
|
||||
captureDisplayFn = screenshot.CaptureDisplay
|
||||
)
|
||||
|
||||
func displayCount() int {
|
||||
return screenshot.NumActiveDisplays()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
var activeDisplays = displayCount
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
func SetCursorCapture(enabled bool) {}
|
||||
|
||||
func DrawCursorOnImage(img *image.RGBA, captureBounds image.Rectangle) {
|
||||
// if you're not windows fuck you
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
cursorCaptureEnabled atomic.Bool
|
||||
)
|
||||
|
||||
func SetCursorCapture(enabled bool) {
|
||||
cursorCaptureEnabled.Store(enabled)
|
||||
}
|
||||
|
||||
var (
|
||||
procGetCursorPos = user32.NewProc("GetCursorPos")
|
||||
procGetCursorInfo = user32.NewProc("GetCursorInfo")
|
||||
procGetIconInfo = user32.NewProc("GetIconInfo")
|
||||
procDrawIconEx = user32.NewProc("DrawIconEx")
|
||||
)
|
||||
|
||||
const (
|
||||
CURSOR_SHOWING = 0x00000001
|
||||
DI_NORMAL = 0x0003
|
||||
DI_DEFAULTSIZE = 0x0008
|
||||
)
|
||||
|
||||
type point struct {
|
||||
x int32
|
||||
y int32
|
||||
}
|
||||
|
||||
type cursorInfo struct {
|
||||
cbSize uint32
|
||||
flags uint32
|
||||
hCursor uintptr
|
||||
ptScreenPos point
|
||||
}
|
||||
|
||||
type iconInfo struct {
|
||||
fIcon uint32
|
||||
xHotspot uint32
|
||||
yHotspot uint32
|
||||
hbmMask uintptr
|
||||
hbmColor uintptr
|
||||
}
|
||||
|
||||
func getCursorPosition() (x, y int32, visible bool) {
|
||||
var ci cursorInfo
|
||||
ci.cbSize = uint32(unsafe.Sizeof(ci))
|
||||
|
||||
ret, _, _ := procGetCursorInfo.Call(uintptr(unsafe.Pointer(&ci)))
|
||||
if ret == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
visible = (ci.flags & CURSOR_SHOWING) != 0
|
||||
return ci.ptScreenPos.x, ci.ptScreenPos.y, visible
|
||||
}
|
||||
|
||||
func drawCursor(img *image.RGBA, cursorX, cursorY int32, bounds image.Rectangle) {
|
||||
imgX := int(cursorX) - bounds.Min.X
|
||||
imgY := int(cursorY) - bounds.Min.Y
|
||||
|
||||
if imgX < 0 || imgY < 0 || imgX >= img.Bounds().Dx() || imgY >= img.Bounds().Dy() {
|
||||
return
|
||||
}
|
||||
|
||||
cursorPattern := []struct {
|
||||
x, y int
|
||||
c color.RGBA
|
||||
}{
|
||||
{0, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 1, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 2, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 3, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 4, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 5, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 6, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 7, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 8, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 9, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 10, color.RGBA{0, 0, 0, 255}},
|
||||
{0, 11, color.RGBA{0, 0, 0, 255}},
|
||||
{1, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{1, 10, color.RGBA{0, 0, 0, 255}},
|
||||
{2, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{2, 9, color.RGBA{0, 0, 0, 255}},
|
||||
{3, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{3, 8, color.RGBA{0, 0, 0, 255}},
|
||||
{3, 9, color.RGBA{0, 0, 0, 255}},
|
||||
{4, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{4, 7, color.RGBA{0, 0, 0, 255}},
|
||||
{4, 8, color.RGBA{0, 0, 0, 255}},
|
||||
{5, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{5, 6, color.RGBA{0, 0, 0, 255}},
|
||||
{5, 7, color.RGBA{0, 0, 0, 255}},
|
||||
{5, 13, color.RGBA{0, 0, 0, 255}},
|
||||
{6, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{6, 5, color.RGBA{0, 0, 0, 255}},
|
||||
{6, 6, color.RGBA{0, 0, 0, 255}},
|
||||
{6, 12, color.RGBA{0, 0, 0, 255}},
|
||||
{6, 13, color.RGBA{0, 0, 0, 255}},
|
||||
{7, 0, color.RGBA{0, 0, 0, 255}},
|
||||
{7, 4, color.RGBA{0, 0, 0, 255}},
|
||||
{7, 5, color.RGBA{0, 0, 0, 255}},
|
||||
{7, 11, color.RGBA{0, 0, 0, 255}},
|
||||
{7, 12, color.RGBA{0, 0, 0, 255}},
|
||||
{8, 4, color.RGBA{0, 0, 0, 255}},
|
||||
{8, 10, color.RGBA{0, 0, 0, 255}},
|
||||
{8, 11, color.RGBA{0, 0, 0, 255}},
|
||||
{9, 4, color.RGBA{0, 0, 0, 255}},
|
||||
{9, 9, color.RGBA{0, 0, 0, 255}},
|
||||
{9, 10, color.RGBA{0, 0, 0, 255}},
|
||||
{10, 8, color.RGBA{0, 0, 0, 255}},
|
||||
{10, 9, color.RGBA{0, 0, 0, 255}},
|
||||
{11, 7, color.RGBA{0, 0, 0, 255}},
|
||||
{11, 8, color.RGBA{0, 0, 0, 255}},
|
||||
{12, 6, color.RGBA{0, 0, 0, 255}},
|
||||
{12, 7, color.RGBA{0, 0, 0, 255}},
|
||||
|
||||
{1, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 8, color.RGBA{255, 255, 255, 255}},
|
||||
{1, 9, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{2, 8, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{3, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{4, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 11, color.RGBA{255, 255, 255, 255}},
|
||||
{5, 12, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 4, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 10, color.RGBA{255, 255, 255, 255}},
|
||||
{6, 11, color.RGBA{255, 255, 255, 255}},
|
||||
{7, 1, color.RGBA{255, 255, 255, 255}},
|
||||
{7, 2, color.RGBA{255, 255, 255, 255}},
|
||||
{7, 3, color.RGBA{255, 255, 255, 255}},
|
||||
{7, 9, color.RGBA{255, 255, 255, 255}},
|
||||
{7, 10, color.RGBA{255, 255, 255, 255}},
|
||||
{8, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{8, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{8, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{8, 8, color.RGBA{255, 255, 255, 255}},
|
||||
{8, 9, color.RGBA{255, 255, 255, 255}},
|
||||
{9, 5, color.RGBA{255, 255, 255, 255}},
|
||||
{9, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{9, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{9, 8, color.RGBA{255, 255, 255, 255}},
|
||||
{10, 6, color.RGBA{255, 255, 255, 255}},
|
||||
{10, 7, color.RGBA{255, 255, 255, 255}},
|
||||
{11, 6, color.RGBA{255, 255, 255, 255}},
|
||||
}
|
||||
|
||||
for _, p := range cursorPattern {
|
||||
px := imgX + p.x
|
||||
py := imgY + p.y
|
||||
if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() {
|
||||
img.SetRGBA(px, py, p.c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DrawCursorOnDC(hdc uintptr, captureBounds image.Rectangle) bool {
|
||||
if !cursorCaptureEnabled.Load() || hdc == 0 {
|
||||
return false
|
||||
}
|
||||
var ci cursorInfo
|
||||
ci.cbSize = uint32(unsafe.Sizeof(ci))
|
||||
ret, _, _ := procGetCursorInfo.Call(uintptr(unsafe.Pointer(&ci)))
|
||||
if ret == 0 || (ci.flags&CURSOR_SHOWING) == 0 || ci.hCursor == 0 {
|
||||
return false
|
||||
}
|
||||
var icon iconInfo
|
||||
ret, _, _ = procGetIconInfo.Call(ci.hCursor, uintptr(unsafe.Pointer(&icon)))
|
||||
if ret == 0 {
|
||||
return false
|
||||
}
|
||||
if icon.hbmMask != 0 {
|
||||
defer deleteObject(icon.hbmMask)
|
||||
}
|
||||
if icon.hbmColor != 0 {
|
||||
defer deleteObject(icon.hbmColor)
|
||||
}
|
||||
|
||||
x := int32(ci.ptScreenPos.x) - int32(captureBounds.Min.X) - int32(icon.xHotspot)
|
||||
y := int32(ci.ptScreenPos.y) - int32(captureBounds.Min.Y) - int32(icon.yHotspot)
|
||||
ret, _, _ = procDrawIconEx.Call(
|
||||
hdc,
|
||||
uintptr(x),
|
||||
uintptr(y),
|
||||
ci.hCursor,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
uintptr(DI_NORMAL),
|
||||
)
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
func DrawCursorOnImage(img *image.RGBA, captureBounds image.Rectangle) {
|
||||
if !cursorCaptureEnabled.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
x, y, visible := getCursorPosition()
|
||||
if visible {
|
||||
drawCursor(img, x, y, captureBounds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
func SetDesktopDuplication(enabled bool) {}
|
||||
@@ -0,0 +1,52 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
)
|
||||
|
||||
func InitializeHVNCDesktop() error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func CleanupHVNCDesktop() {}
|
||||
|
||||
func SetHVNCCursorCapture(enabled bool) {}
|
||||
|
||||
func hvncCaptureDisplay(display int) (*image.RGBA, error) {
|
||||
return nil, errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCMonitorCount() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func StartHVNCProcess(filePath string) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputMouseMove(display int, x, y int32) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputMouseDown(button int) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputMouseUp(button int) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputKeyDown(vk uint16) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputKeyUp(vk uint16) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
|
||||
func HVNCInputMouseWheel(delta int32) error {
|
||||
return errors.New("HVNC not supported on this platform")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
)
|
||||
|
||||
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build turbojpeg
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
|
||||
ljpeg "github.com/pixiv/go-libjpeg/jpeg"
|
||||
)
|
||||
|
||||
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
||||
opts := &ljpeg.EncoderOptions{
|
||||
Quality: quality,
|
||||
OptimizeCoding: true,
|
||||
DCTMethod: ljpeg.DCTISlow,
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
if err := ljpeg.Encode(&buf, img, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package capture
|
||||
|
||||
import "image"
|
||||
|
||||
type MonitorInfo struct {
|
||||
Width int `msgpack:"width"`
|
||||
Height int `msgpack:"height"`
|
||||
}
|
||||
|
||||
func DisplayBounds(idx int) image.Rectangle {
|
||||
return displayBounds(idx)
|
||||
}
|
||||
|
||||
func DisplayScale(idx int) float64 {
|
||||
return displayScale(idx)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
func ResetMonitorCache() {}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
import "image"
|
||||
|
||||
func displayBounds(idx int) image.Rectangle {
|
||||
return image.Rectangle{}
|
||||
}
|
||||
|
||||
func displayScale(idx int) float64 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
func MonitorInfos() []MonitorInfo {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
|
||||
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
||||
procEnumDisplaySettingsW = user32.NewProc("EnumDisplaySettingsW")
|
||||
)
|
||||
|
||||
type rect struct {
|
||||
left int32
|
||||
top int32
|
||||
right int32
|
||||
bottom int32
|
||||
}
|
||||
|
||||
type monitorInfoEx struct {
|
||||
cbSize uint32
|
||||
rcMonitor rect
|
||||
rcWork rect
|
||||
dwFlags uint32
|
||||
szDevice [32]uint16
|
||||
}
|
||||
|
||||
type devMode struct {
|
||||
dmDeviceName [32]uint16
|
||||
dmSpecVersion uint16
|
||||
dmDriverVersion uint16
|
||||
dmSize uint16
|
||||
dmDriverExtra uint16
|
||||
dmFields uint32
|
||||
dmPositionX int32
|
||||
dmPositionY int32
|
||||
dmDisplayOrientation uint32
|
||||
dmDisplayFixedOutput uint32
|
||||
dmColor int16
|
||||
dmDuplex int16
|
||||
dmYResolution int16
|
||||
dmTTOption int16
|
||||
dmCollate int16
|
||||
dmFormName [32]uint16
|
||||
dmLogPixels uint16
|
||||
dmBitsPerPel uint32
|
||||
dmPelsWidth uint32
|
||||
dmPelsHeight uint32
|
||||
dmDisplayFlags uint32
|
||||
dmDisplayFrequency uint32
|
||||
dmICMMethod uint32
|
||||
dmICMIntent uint32
|
||||
dmMediaType uint32
|
||||
dmDitherType uint32
|
||||
dmReserved1 uint32
|
||||
dmReserved2 uint32
|
||||
dmPanningWidth uint32
|
||||
dmPanningHeight uint32
|
||||
}
|
||||
|
||||
const (
|
||||
monitorInfofPrimary = 1
|
||||
enumCurrentSettings = 0xFFFFFFFF
|
||||
)
|
||||
|
||||
type monitorDesc struct {
|
||||
name string
|
||||
rect image.Rectangle
|
||||
workRect image.Rectangle
|
||||
primary bool
|
||||
physW int
|
||||
physH int
|
||||
scale float64
|
||||
posX int
|
||||
posY int
|
||||
}
|
||||
|
||||
var (
|
||||
monitorsCache atomic.Value
|
||||
monitorsMu sync.Mutex
|
||||
monitorsTs int64
|
||||
)
|
||||
|
||||
func displayCount() int {
|
||||
setDPIAware()
|
||||
return len(monitorList())
|
||||
}
|
||||
|
||||
func displayBounds(idx int) image.Rectangle {
|
||||
mons := monitorList()
|
||||
if idx >= 0 && idx < len(mons) {
|
||||
return mons[idx].rect
|
||||
}
|
||||
|
||||
if len(mons) > 0 {
|
||||
return mons[0].rect
|
||||
}
|
||||
|
||||
w := int(getSystemMetric(SM_CXSCREEN))
|
||||
h := int(getSystemMetric(SM_CYSCREEN))
|
||||
return image.Rect(0, 0, w, h)
|
||||
}
|
||||
|
||||
func displayScale(idx int) float64 {
|
||||
mons := monitorList()
|
||||
if idx >= 0 && idx < len(mons) {
|
||||
if mons[idx].scale > 0 {
|
||||
return mons[idx].scale
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
func monitorList() []monitorDesc {
|
||||
setDPIAware()
|
||||
if v, ok := monitorsCache.Load().([]monitorDesc); ok && len(v) > 0 && time.Since(time.Unix(0, monitorsTs)) < 30*time.Second {
|
||||
return v
|
||||
}
|
||||
|
||||
monitorsMu.Lock()
|
||||
defer monitorsMu.Unlock()
|
||||
|
||||
if v, ok := monitorsCache.Load().([]monitorDesc); ok && len(v) > 0 && time.Since(time.Unix(0, monitorsTs)) < 30*time.Second {
|
||||
return v
|
||||
}
|
||||
|
||||
mons := enumerateMonitors()
|
||||
monitorsCache.Store(mons)
|
||||
monitorsTs = time.Now().UnixNano()
|
||||
logMonitors(mons)
|
||||
return mons
|
||||
}
|
||||
|
||||
func ResetMonitorCache() {
|
||||
monitorsMu.Lock()
|
||||
defer monitorsMu.Unlock()
|
||||
monitorsCache.Store([]monitorDesc{})
|
||||
monitorsTs = 0
|
||||
}
|
||||
|
||||
func enumerateMonitors() []monitorDesc {
|
||||
var result []monitorDesc
|
||||
|
||||
cb := syscall.NewCallback(func(hMonitor, hdc, lprcMonitor, lparam uintptr) uintptr {
|
||||
var mi monitorInfoEx
|
||||
mi.cbSize = uint32(unsafe.Sizeof(mi))
|
||||
if ret, _, _ := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&mi))); ret == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
name := windows.UTF16ToString(mi.szDevice[:])
|
||||
primary := mi.dwFlags&monitorInfofPrimary != 0
|
||||
|
||||
var dm devMode
|
||||
dm.dmSize = uint16(unsafe.Sizeof(dm))
|
||||
var physW, physH int
|
||||
var scale float64 = 1.0
|
||||
var posX, posY int
|
||||
if ret, _, _ := procEnumDisplaySettingsW.Call(uintptr(unsafe.Pointer(&mi.szDevice[0])), enumCurrentSettings, uintptr(unsafe.Pointer(&dm))); ret != 0 {
|
||||
physW = int(dm.dmPelsWidth)
|
||||
physH = int(dm.dmPelsHeight)
|
||||
posX = int(dm.dmPositionX)
|
||||
posY = int(dm.dmPositionY)
|
||||
vw := int(mi.rcMonitor.right - mi.rcMonitor.left)
|
||||
if vw > 0 {
|
||||
scale = float64(dm.dmPelsWidth) / float64(vw)
|
||||
}
|
||||
}
|
||||
|
||||
r := image.Rect(int(mi.rcMonitor.left), int(mi.rcMonitor.top), int(mi.rcMonitor.right), int(mi.rcMonitor.bottom))
|
||||
w := image.Rect(int(mi.rcWork.left), int(mi.rcWork.top), int(mi.rcWork.right), int(mi.rcWork.bottom))
|
||||
|
||||
result = append(result, monitorDesc{
|
||||
name: name,
|
||||
rect: r,
|
||||
workRect: w,
|
||||
primary: primary,
|
||||
physW: physW,
|
||||
physH: physH,
|
||||
scale: scale,
|
||||
posX: posX,
|
||||
posY: posY,
|
||||
})
|
||||
return 1
|
||||
})
|
||||
|
||||
if ret, _, _ := procEnumDisplayMonitors.Call(0, 0, cb, 0); ret == 0 {
|
||||
|
||||
w := int(getSystemMetric(SM_CXSCREEN))
|
||||
h := int(getSystemMetric(SM_CYSCREEN))
|
||||
return []monitorDesc{{
|
||||
name: "Primary",
|
||||
rect: image.Rect(0, 0, w, h),
|
||||
primary: true,
|
||||
physW: w,
|
||||
physH: h,
|
||||
scale: 1.0,
|
||||
}}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].primary != result[j].primary {
|
||||
return result[i].primary
|
||||
}
|
||||
oi := deviceOrdinal(result[i].name)
|
||||
oj := deviceOrdinal(result[j].name)
|
||||
if oi >= 0 && oj >= 0 && oi != oj {
|
||||
return oi < oj
|
||||
}
|
||||
if result[i].rect.Min.Y == result[j].rect.Min.Y {
|
||||
return result[i].rect.Min.X < result[j].rect.Min.X
|
||||
}
|
||||
return result[i].rect.Min.Y < result[j].rect.Min.Y
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func deviceOrdinal(name string) int {
|
||||
name = strings.TrimSpace(name)
|
||||
end := len(name)
|
||||
start := end
|
||||
for start > 0 {
|
||||
c := name[start-1]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
start--
|
||||
}
|
||||
if start == end {
|
||||
return -1
|
||||
}
|
||||
n, err := strconv.Atoi(name[start:end])
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func logMonitors(mons []monitorDesc) {
|
||||
log.Printf("capture: detected %d monitor(s)", len(mons))
|
||||
for i, m := range mons {
|
||||
log.Printf("capture: monitor %d name=%q primary=%v virtual=%v phys=%dx%d scale=%.2f pos=(%d,%d)", i, m.name, m.primary, m.rect, m.physW, m.physH, m.scale, m.posX, m.posY)
|
||||
}
|
||||
}
|
||||
|
||||
func MonitorInfos() []MonitorInfo {
|
||||
mons := monitorList()
|
||||
if len(mons) == 0 {
|
||||
return nil
|
||||
}
|
||||
infos := make([]MonitorInfo, 0, len(mons))
|
||||
for _, m := range mons {
|
||||
w := m.rect.Dx()
|
||||
h := m.rect.Dy()
|
||||
if w < 0 {
|
||||
w = 0
|
||||
}
|
||||
if h < 0 {
|
||||
h = 0
|
||||
}
|
||||
infos = append(infos, MonitorInfo{Width: w, Height: h})
|
||||
}
|
||||
return infos
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
const crashLogFileName = "crashlogC.log"
|
||||
|
||||
func goSafe(label string, cancel context.CancelFunc, fn func()) {
|
||||
go func() {
|
||||
defer recoverAndLog(label, cancel)
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
func recoverAndLog(label string, cancel context.CancelFunc) {
|
||||
if r := recover(); r != nil {
|
||||
reason := fmt.Sprintf("%s panic: %v", label, r)
|
||||
stack := debug.Stack()
|
||||
path := writeCrashLog(reason, stack)
|
||||
log.Printf("[panic] %s (see %s)", reason, path)
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeCrashLog(reason string, stack []byte) string {
|
||||
dir := os.TempDir()
|
||||
_ = os.MkdirAll(dir, 0700)
|
||||
path := filepath.Join(dir, crashLogFileName)
|
||||
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Printf("[panic] failed to open crash log %s: %v", path, err)
|
||||
return path
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
timestamp := time.Now().UTC().Format(time.RFC3339)
|
||||
_, _ = fmt.Fprintf(file, "\n=== %s ===\n", timestamp)
|
||||
_, _ = fmt.Fprintf(file, "GOOS=%s GOARCH=%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
_, _ = fmt.Fprintf(file, "Reason: %s\n", reason)
|
||||
if len(stack) > 0 {
|
||||
_, _ = fmt.Fprintf(file, "Stack:\n%s\n", string(stack))
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
func ResetDesktopCapture() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
func ResetDesktopCapture() {
|
||||
// Prevent freeing the DIB section while a BitBlt capture is in progress.
|
||||
captureMu.Lock()
|
||||
defer captureMu.Unlock()
|
||||
state.reset()
|
||||
dxgiState.reset()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package capture
|
||||
|
||||
import "image"
|
||||
|
||||
func cursorROI() (image.Rectangle, bool) {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
|
||||
func focusWindowROI() (image.Rectangle, bool) {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
procGetForegroundWindowROI = user32.NewProc("GetForegroundWindow")
|
||||
procGetWindowRectROI = user32.NewProc("GetWindowRect")
|
||||
)
|
||||
|
||||
type winRect struct {
|
||||
left int32
|
||||
top int32
|
||||
right int32
|
||||
bottom int32
|
||||
}
|
||||
|
||||
func cursorROI() (image.Rectangle, bool) {
|
||||
x, y, visible := getCursorPosition()
|
||||
if !visible {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
const cursorBox = 48
|
||||
half := cursorBox / 2
|
||||
r := image.Rect(int(x)-half, int(y)-half, int(x)+half, int(y)+half)
|
||||
if r.Dx() <= 0 || r.Dy() <= 0 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
func focusWindowROI() (image.Rectangle, bool) {
|
||||
hwnd, _, _ := procGetForegroundWindowROI.Call()
|
||||
if hwnd == 0 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
var wr winRect
|
||||
ret, _, _ := procGetWindowRectROI.Call(hwnd, uintptr(unsafe.Pointer(&wr)))
|
||||
if ret == 0 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
r := image.Rect(int(wr.left), int(wr.top), int(wr.right), int(wr.bottom))
|
||||
if r.Dx() <= 0 || r.Dy() <= 0 {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
shcore = syscall.NewLazyDLL("shcore.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetDC = user32.NewProc("GetDC")
|
||||
procReleaseDC = user32.NewProc("ReleaseDC")
|
||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
||||
procCreateDCW = gdi32.NewProc("CreateDCW")
|
||||
procDeleteDC = gdi32.NewProc("DeleteDC")
|
||||
procCreateCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap")
|
||||
procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
||||
procDeleteObject = gdi32.NewProc("DeleteObject")
|
||||
procSelectObject = gdi32.NewProc("SelectObject")
|
||||
procBitBlt = gdi32.NewProc("BitBlt")
|
||||
procSetProcessDpiAwareness = shcore.NewProc("SetProcessDpiAwareness")
|
||||
)
|
||||
|
||||
const (
|
||||
SM_CXSCREEN = 0
|
||||
SM_CYSCREEN = 1
|
||||
SM_XVIRTUALSCREEN = 76
|
||||
SM_YVIRTUALSCREEN = 77
|
||||
SM_CXVIRTUALSCREEN = 78
|
||||
SM_CYVIRTUALSCREEN = 79
|
||||
SRCCOPY = 0x00CC0020
|
||||
CAPTUREBLT = 0x40000000
|
||||
BI_RGB = 0
|
||||
DIB_RGB_COLORS = 0
|
||||
PROCESS_PER_MONITOR_DPI_AWARE = 2
|
||||
)
|
||||
|
||||
type bitmapInfoHeader struct {
|
||||
biSize uint32
|
||||
biWidth int32
|
||||
biHeight int32
|
||||
biPlanes uint16
|
||||
biBitCount uint16
|
||||
biCompression uint32
|
||||
biSizeImage uint32
|
||||
biXPelsPerMeter int32
|
||||
biYPelsPerMeter int32
|
||||
biClrUsed uint32
|
||||
biClrImportant uint32
|
||||
}
|
||||
|
||||
type bitmapInfo struct {
|
||||
bmiHeader bitmapInfoHeader
|
||||
redMask uint32
|
||||
greenMask uint32
|
||||
blueMask uint32
|
||||
alphaMask uint32
|
||||
}
|
||||
|
||||
func getSystemMetric(i int32) int32 {
|
||||
r, _, _ := procGetSystemMetrics.Call(uintptr(i))
|
||||
return int32(r)
|
||||
}
|
||||
|
||||
func getDC(hwnd uintptr) uintptr {
|
||||
dc, _, _ := procGetDC.Call(hwnd)
|
||||
return dc
|
||||
}
|
||||
|
||||
func releaseDC(hwnd, hdc uintptr) {
|
||||
_, _, _ = procReleaseDC.Call(hwnd, hdc)
|
||||
}
|
||||
|
||||
func createCompatibleDC(hdc uintptr) uintptr {
|
||||
r, _, _ := procCreateCompatibleDC.Call(hdc)
|
||||
return r
|
||||
}
|
||||
|
||||
func createDisplayDC() uintptr {
|
||||
displayName := syscall.StringToUTF16Ptr("DISPLAY")
|
||||
r, _, _ := procCreateDCW.Call(uintptr(unsafe.Pointer(displayName)), 0, 0, 0)
|
||||
return r
|
||||
}
|
||||
|
||||
func deleteDC(hdc uintptr) {
|
||||
_, _, _ = procDeleteDC.Call(hdc)
|
||||
}
|
||||
|
||||
func createCompatibleBitmap(hdc uintptr, w, h int32) uintptr {
|
||||
r, _, _ := procCreateCompatibleBitmap.Call(hdc, uintptr(w), uintptr(h))
|
||||
return r
|
||||
}
|
||||
|
||||
func createDIBSection(hdc uintptr, bmi *bitmapInfo, usage uint32, bits *unsafe.Pointer) uintptr {
|
||||
r, _, _ := procCreateDIBSection.Call(hdc, uintptr(unsafe.Pointer(bmi)), uintptr(usage), uintptr(unsafe.Pointer(bits)), 0, 0)
|
||||
return r
|
||||
}
|
||||
|
||||
func deleteObject(obj uintptr) {
|
||||
_, _, _ = procDeleteObject.Call(obj)
|
||||
}
|
||||
|
||||
func selectObject(hdc, hgdiobj uintptr) uintptr {
|
||||
r, _, _ := procSelectObject.Call(hdc, hgdiobj)
|
||||
return r
|
||||
}
|
||||
|
||||
func bitBlt(hdcDest uintptr, x, y, cx, cy int32, hdcSrc uintptr, x1, y1 int32, rop uint32) bool {
|
||||
r, _, _ := procBitBlt.Call(hdcDest, uintptr(x), uintptr(y), uintptr(cx), uintptr(cy), hdcSrc, uintptr(x1), uintptr(y1), uintptr(rop))
|
||||
return r != 0
|
||||
}
|
||||
|
||||
func bitBltWithFallback(hdcMem, hdcScreen uintptr, bounds image.Rectangle, w, h int) bool {
|
||||
if bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY|CAPTUREBLT) {
|
||||
return true
|
||||
}
|
||||
return bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY)
|
||||
}
|
||||
|
||||
var dpiAwareOnce sync.Once
|
||||
|
||||
func setDPIAware() {
|
||||
dpiAwareOnce.Do(func() {
|
||||
|
||||
if procSetProcessDpiAwareness.Find() == nil {
|
||||
procSetProcessDpiAwareness.Call(uintptr(PROCESS_PER_MONITOR_DPI_AWARE))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
captureCount atomic.Int64
|
||||
bitbltNs atomic.Int64
|
||||
convertNs atomic.Int64
|
||||
lastCaptureLogNs atomic.Int64
|
||||
scaleOnce sync.Once
|
||||
cachedScale float64
|
||||
state capState
|
||||
captureMu sync.Mutex
|
||||
)
|
||||
|
||||
var captureDisplayFn = func(display int) (*image.RGBA, error) {
|
||||
if useDesktopDuplication() {
|
||||
img, err := captureDisplayDXGI(display)
|
||||
if err == nil && img != nil {
|
||||
return img, nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("capture: dxgi duplication failed: %v (falling back to bitblt)", err)
|
||||
}
|
||||
}
|
||||
return captureDisplayBitBlt(display)
|
||||
}
|
||||
|
||||
func captureDisplayBitBlt(display int) (*image.RGBA, error) {
|
||||
|
||||
captureMu.Lock()
|
||||
defer captureMu.Unlock()
|
||||
|
||||
setDPIAware()
|
||||
|
||||
maxDisplays := displayCount()
|
||||
if display < 0 || display >= maxDisplays {
|
||||
display = 0
|
||||
}
|
||||
|
||||
mons := monitorList()
|
||||
if display >= len(mons) {
|
||||
display = 0
|
||||
}
|
||||
mon := mons[display]
|
||||
|
||||
bounds := resolveBounds(mon)
|
||||
srcW := bounds.Dx()
|
||||
srcH := bounds.Dy()
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
ResetMonitorCache()
|
||||
mons = monitorList()
|
||||
if display >= len(mons) {
|
||||
display = 0
|
||||
}
|
||||
if display < len(mons) {
|
||||
mon = mons[display]
|
||||
bounds = resolveBounds(mon)
|
||||
srcW = bounds.Dx()
|
||||
srcH = bounds.Dy()
|
||||
}
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
userScale := captureScale()
|
||||
dstW := int(float64(srcW) * userScale)
|
||||
dstH := int(float64(srcH) * userScale)
|
||||
if dstW <= 0 || dstH <= 0 {
|
||||
dstW = srcW
|
||||
dstH = srcH
|
||||
}
|
||||
|
||||
capW := srcW
|
||||
capH := srcH
|
||||
|
||||
hdcScreen := createDisplayDC()
|
||||
fromCreateDC := hdcScreen != 0
|
||||
if hdcScreen == 0 {
|
||||
hdcScreen = getDC(0)
|
||||
}
|
||||
if hdcScreen == 0 {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
closeDC := func() {
|
||||
if hdcScreen == 0 {
|
||||
return
|
||||
}
|
||||
if fromCreateDC {
|
||||
deleteDC(hdcScreen)
|
||||
} else {
|
||||
releaseDC(0, hdcScreen)
|
||||
}
|
||||
hdcScreen = 0
|
||||
}
|
||||
defer closeDC()
|
||||
|
||||
if capW <= 0 || capH <= 0 {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
|
||||
captureWithDC := func() (*image.RGBA, time.Duration, time.Duration, time.Duration, error) {
|
||||
hdcMem, _, buf, stride, err := state.ensure(hdcScreen, capW, capH)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, err
|
||||
}
|
||||
|
||||
bitStart := time.Now()
|
||||
if !bitBltWithFallback(hdcMem, hdcScreen, bounds, capW, capH) {
|
||||
state.reset()
|
||||
hdcMem, _, buf, stride, err = state.ensure(hdcScreen, capW, capH)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, err
|
||||
}
|
||||
if !bitBltWithFallback(hdcMem, hdcScreen, bounds, capW, capH) {
|
||||
return nil, 0, 0, 0, syscall.EINVAL
|
||||
}
|
||||
}
|
||||
bitDur := time.Since(bitStart)
|
||||
cursorDrawn := DrawCursorOnDC(hdcMem, bounds)
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil, 0, 0, 0, syscall.EINVAL
|
||||
}
|
||||
|
||||
dibDur := time.Duration(0)
|
||||
|
||||
convStart := time.Now()
|
||||
swapRB(buf)
|
||||
img := &image.RGBA{Pix: buf, Stride: stride, Rect: image.Rect(0, 0, capW, capH)}
|
||||
convDur := time.Since(convStart)
|
||||
|
||||
if !cursorDrawn {
|
||||
DrawCursorOnImage(img, bounds)
|
||||
}
|
||||
if dstW != capW || dstH != capH {
|
||||
img = resizeNearest(img, dstW, dstH)
|
||||
} else {
|
||||
// Detach from the reusable DIB buffer to avoid concurrent mutation during encode.
|
||||
stable := image.NewRGBA(img.Rect)
|
||||
copy(stable.Pix, img.Pix)
|
||||
img = stable
|
||||
}
|
||||
|
||||
return img, bitDur, dibDur, convDur, nil
|
||||
}
|
||||
|
||||
img, bitDur, dibDur, convDur, err := captureWithDC()
|
||||
if err != nil && fromCreateDC {
|
||||
closeDC()
|
||||
hdcScreen = getDC(0)
|
||||
fromCreateDC = false
|
||||
if hdcScreen == 0 {
|
||||
return nil, err
|
||||
}
|
||||
img, bitDur, dibDur, convDur, err = captureWithDC()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logCaptureTimings(bitDur, dibDur, convDur)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func clampToVirtual(bounds image.Rectangle) image.Rectangle {
|
||||
vx := int(getSystemMetric(SM_XVIRTUALSCREEN))
|
||||
vy := int(getSystemMetric(SM_YVIRTUALSCREEN))
|
||||
vw := int(getSystemMetric(SM_CXVIRTUALSCREEN))
|
||||
vh := int(getSystemMetric(SM_CYVIRTUALSCREEN))
|
||||
if vw <= 0 || vh <= 0 {
|
||||
return bounds
|
||||
}
|
||||
virtual := image.Rect(vx, vy, vx+vw, vy+vh)
|
||||
inter := bounds.Intersect(virtual)
|
||||
if inter.Empty() {
|
||||
return bounds
|
||||
}
|
||||
return inter
|
||||
}
|
||||
|
||||
func resolveBounds(mon monitorDesc) image.Rectangle {
|
||||
virtualW := int(getSystemMetric(SM_CXVIRTUALSCREEN))
|
||||
virtualH := int(getSystemMetric(SM_CYVIRTUALSCREEN))
|
||||
if virtualW > 0 && virtualH > 0 {
|
||||
return clampToVirtual(mon.rect)
|
||||
}
|
||||
return captureBounds(mon)
|
||||
}
|
||||
|
||||
func captureBounds(mon monitorDesc) image.Rectangle {
|
||||
bounds := mon.rect
|
||||
if mon.physW > 0 && mon.physH > 0 {
|
||||
phys := image.Rect(mon.posX, mon.posY, mon.posX+mon.physW, mon.posY+mon.physH)
|
||||
if phys.Dx() > 0 && phys.Dy() > 0 {
|
||||
if mon.scale != 1.0 || phys.Dx() != bounds.Dx() || phys.Dy() != bounds.Dy() {
|
||||
bounds = phys
|
||||
}
|
||||
}
|
||||
}
|
||||
return bounds
|
||||
}
|
||||
|
||||
func swapRB(pix []byte) {
|
||||
for i := 0; i+3 < len(pix); i += 4 {
|
||||
pix[i], pix[i+2] = pix[i+2], pix[i]
|
||||
}
|
||||
}
|
||||
|
||||
func logCaptureTimings(bitDur, dibDur, convDur time.Duration) {
|
||||
captureCount.Add(1)
|
||||
bitbltNs.Add(bitDur.Nanoseconds())
|
||||
convertNs.Add(convDur.Nanoseconds())
|
||||
|
||||
nowNs := time.Now().UnixNano()
|
||||
last := lastCaptureLogNs.Load()
|
||||
if last != 0 && time.Duration(nowNs-last) < 5*time.Second {
|
||||
return
|
||||
}
|
||||
if !lastCaptureLogNs.CompareAndSwap(last, nowNs) {
|
||||
return
|
||||
}
|
||||
frames := captureCount.Swap(0)
|
||||
if frames == 0 {
|
||||
return
|
||||
}
|
||||
avg := func(totalNs atomic.Int64) float64 {
|
||||
return float64(totalNs.Swap(0)) / 1e6 / float64(frames)
|
||||
}
|
||||
bitAvg := avg(bitbltNs)
|
||||
convAvg := avg(convertNs)
|
||||
log.Printf("capture: win bitblt avg bitblt=%.2fms convert=%.2fms frames=%d", bitAvg, convAvg, frames)
|
||||
}
|
||||
|
||||
func resizeNearest(src *image.RGBA, w, h int) *image.RGBA {
|
||||
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
srcW := src.Bounds().Dx()
|
||||
srcH := src.Bounds().Dy()
|
||||
for y := 0; y < h; y++ {
|
||||
sy := y * srcH / h
|
||||
sp := sy * src.Stride
|
||||
dp := y * dst.Stride
|
||||
for x := 0; x < w; x++ {
|
||||
sx := x * srcW / w
|
||||
s := sp + sx*4
|
||||
d := dp + x*4
|
||||
dst.Pix[d+0] = src.Pix[s+0]
|
||||
dst.Pix[d+1] = src.Pix[s+1]
|
||||
dst.Pix[d+2] = src.Pix[s+2]
|
||||
dst.Pix[d+3] = src.Pix[s+3]
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
type capState struct {
|
||||
mu sync.Mutex
|
||||
hdcMem uintptr
|
||||
hbmp uintptr
|
||||
buf []byte
|
||||
stride int
|
||||
w int
|
||||
h int
|
||||
bits unsafe.Pointer
|
||||
}
|
||||
|
||||
func (s *capState) ensure(hdcScreen uintptr, w, h int) (uintptr, uintptr, []byte, int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.hdcMem == 0 {
|
||||
s.hdcMem = createCompatibleDC(hdcScreen)
|
||||
if s.hdcMem == 0 {
|
||||
return 0, 0, nil, 0, syscall.EINVAL
|
||||
}
|
||||
}
|
||||
|
||||
if s.w != w || s.h != h || s.hbmp == 0 {
|
||||
bmi := bitmapInfo{
|
||||
bmiHeader: bitmapInfoHeader{
|
||||
biSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||
biWidth: int32(w),
|
||||
biHeight: -int32(h),
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
},
|
||||
}
|
||||
var bits unsafe.Pointer
|
||||
newBmp := createDIBSection(s.hdcMem, &bmi, DIB_RGB_COLORS, &bits)
|
||||
if newBmp == 0 || bits == nil {
|
||||
return 0, 0, nil, 0, syscall.EINVAL
|
||||
}
|
||||
selectObject(s.hdcMem, newBmp)
|
||||
if s.hbmp != 0 {
|
||||
deleteObject(s.hbmp)
|
||||
}
|
||||
s.hbmp = newBmp
|
||||
s.w = w
|
||||
s.h = h
|
||||
s.stride = w * 4
|
||||
s.bits = bits
|
||||
s.buf = unsafe.Slice((*byte)(bits), s.stride*h)
|
||||
}
|
||||
|
||||
return s.hdcMem, s.hbmp, s.buf, s.stride, nil
|
||||
}
|
||||
|
||||
func (s *capState) reset() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.hbmp != 0 {
|
||||
deleteObject(s.hbmp)
|
||||
s.hbmp = 0
|
||||
}
|
||||
if s.hdcMem != 0 {
|
||||
deleteDC(s.hdcMem)
|
||||
s.hdcMem = 0
|
||||
}
|
||||
s.buf = nil
|
||||
s.stride = 0
|
||||
s.w = 0
|
||||
s.h = 0
|
||||
s.bits = nil
|
||||
}
|
||||
|
||||
func captureScale() float64 {
|
||||
scaleOnce.Do(func() {
|
||||
s := 1.0
|
||||
if env := os.Getenv("OVERLORD_RD_SCALE"); env != "" {
|
||||
if v, err := strconv.ParseFloat(env, 64); err == nil && v > 0.2 && v <= 1.5 {
|
||||
s = v
|
||||
}
|
||||
}
|
||||
cachedScale = s
|
||||
})
|
||||
return cachedScale
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDesktopDuplicationToggleResetsState(t *testing.T) {
|
||||
dxgiState.mu.Lock()
|
||||
dxgiState.display = 0
|
||||
dxgiState.outputName = "test-output"
|
||||
dxgiState.bounds = image.Rect(1, 2, 3, 4)
|
||||
dxgiState.mu.Unlock()
|
||||
|
||||
SetDesktopDuplication(true)
|
||||
if !useDesktopDuplication() {
|
||||
t.Fatalf("expected desktop duplication to be enabled")
|
||||
}
|
||||
|
||||
dxgiState.mu.Lock()
|
||||
if dxgiState.display != -1 {
|
||||
dxgiState.mu.Unlock()
|
||||
t.Fatalf("expected display reset to -1, got %d", dxgiState.display)
|
||||
}
|
||||
if dxgiState.outputName != "" {
|
||||
dxgiState.mu.Unlock()
|
||||
t.Fatalf("expected outputName reset, got %q", dxgiState.outputName)
|
||||
}
|
||||
if dxgiState.bounds != (image.Rectangle{}) {
|
||||
dxgiState.mu.Unlock()
|
||||
t.Fatalf("expected bounds reset, got %+v", dxgiState.bounds)
|
||||
}
|
||||
dxgiState.mu.Unlock()
|
||||
|
||||
SetDesktopDuplication(false)
|
||||
if useDesktopDuplication() {
|
||||
t.Fatalf("expected desktop duplication to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureDisplayDXGI(t *testing.T) {
|
||||
if displayCount() == 0 {
|
||||
t.Skip("no displays detected")
|
||||
}
|
||||
|
||||
SetDesktopDuplication(true)
|
||||
t.Cleanup(func() { SetDesktopDuplication(false) })
|
||||
|
||||
img, err := captureDisplayDXGI(0)
|
||||
if err != nil {
|
||||
t.Fatalf("dxgi capture failed: %v", err)
|
||||
}
|
||||
if img == nil {
|
||||
t.Fatalf("dxgi capture returned nil image")
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
||||
t.Fatalf("invalid capture bounds: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user