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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user