Files
2026-03-05 00:04:26 -06:00

279 lines
6.0 KiB
Go

//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
}