HEVC (and some other small shit)

This commit is contained in:
KingKDot
2026-07-18 01:27:58 -05:00
parent b619658a95
commit ae74e17d13
34 changed files with 1129 additions and 101 deletions
+49 -2
View File
@@ -111,7 +111,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
if frame, captureDur, encodeDur, used, err := tryBuildDirectH264Frame(display); used {
if err != nil {
consecutiveCaptureFails.Add(1)
log.Printf("capture: direct h264 capture failed: %v (sending black frame, consecutive=%d)", err, consecutiveCaptureFails.Load())
log.Printf("capture: direct video capture failed: %v (sending black frame, consecutive=%d)", err, consecutiveCaptureFails.Load())
return sendBlackFrame(ctx, env)
}
if len(frame.Data) == 0 {
@@ -513,6 +513,7 @@ var (
overrideCodec atomic.Value
desktopSoftwareH264 atomic.Bool
h264WarnOnce sync.Once
hevcWarnOnce sync.Once
codecLogOnce sync.Once
prevMu sync.Mutex
@@ -756,6 +757,33 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
full := false
now := time.Now()
codec := blockCodec()
if codec == "hevc" {
if width%2 != 0 || height%2 != 0 {
log.Printf("capture: hevc skipped for odd dimensions (%dx%d), falling back to h264/jpeg", width, height)
codec = "h264"
} else {
if webrtcpub.ConsumeKeyframeRequest() {
resetHEVCEncoder()
}
hevcBytes, err := encodeHEVCFrame(img)
if err == nil && len(hevcBytes) > 0 {
lastKeyframe.Store(now.UnixNano())
statFullFrames.Add(1)
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "hevc"}, Data: hevcBytes}, time.Since(encStart), nil
}
hevcWarnOnce.Do(func() {
log.Printf("capture: hevc encode unavailable, falling back to h264/jpeg: %v (%s)", err, hevcAvailabilityDetail())
})
resetHEVCEncoder()
if h264Available() {
codec = "h264"
overrideCodec.Store("h264")
} else {
codec = "jpeg"
overrideCodec.Store("jpeg")
}
}
}
if codec == "h264" {
if width%2 != 0 || height%2 != 0 {
@@ -1567,6 +1595,8 @@ func blockCodec() string {
switch codec {
case "h264":
cachedBlockCodec = "h264"
case "hevc", "h265":
cachedBlockCodec = "hevc"
case "raw", "rgba":
cachedBlockCodec = "raw"
case "jpeg", "":
@@ -1586,6 +1616,18 @@ func SetQualityAndCodec(quality int, codec string) {
overrideQuality.Store(int64(quality))
}
s := strings.ToLower(strings.TrimSpace(codec))
if s == "h265" {
s = "hevc"
}
if s == "hevc" && !hevcAvailable() {
detail := hevcAvailabilityDetail()
fallback := "jpeg"
if h264Available() {
fallback = "h264"
}
log.Printf("capture: requested codec=hevc but unavailable (%s); forcing codec=%s", detail, fallback)
s = fallback
}
if s == "h264" && !h264Available() {
detail := h264AvailabilityDetail()
if detail != "" {
@@ -1596,19 +1638,24 @@ func SetQualityAndCodec(quality int, codec string) {
s = "jpeg"
}
switch s {
case "raw", "rgba", "jpeg", "h264":
case "raw", "rgba", "jpeg", "h264", "hevc":
overrideCodec.Store(s)
h264WarnOnce = sync.Once{}
hevcWarnOnce = sync.Once{}
if s != "h264" {
resetH264Encoder()
resetH264Encoderbackstage()
}
if s != "hevc" {
resetHEVCEncoder()
}
case "":
default:
overrideCodec.Store("jpeg")
resetH264Encoder()
resetH264Encoderbackstage()
resetHEVCEncoder()
}
}
@@ -9,9 +9,59 @@ type DesktopEncoderProfile struct {
Providers []string `msgpack:"providers" json:"providers"`
}
type DesktopCodecCapability struct {
Codec string `msgpack:"codec" json:"codec"`
Encoders []string `msgpack:"encoders,omitempty" json:"encoders,omitempty"`
Transports []string `msgpack:"transports" json:"transports"`
Hardware bool `msgpack:"hardware,omitempty" json:"hardware,omitempty"`
}
type DesktopEncoderCapabilities struct {
Probed bool `msgpack:"probed" json:"probed"`
Display int `msgpack:"display" json:"display"`
Profiles []DesktopEncoderProfile `msgpack:"profiles" json:"profiles"`
Detail string `msgpack:"detail,omitempty" json:"detail,omitempty"`
Probed bool `msgpack:"probed" json:"probed"`
Display int `msgpack:"display" json:"display"`
Profiles []DesktopEncoderProfile `msgpack:"profiles" json:"profiles"`
Codecs []DesktopCodecCapability `msgpack:"codecs" json:"codecs"`
Detail string `msgpack:"detail,omitempty" json:"detail,omitempty"`
}
func completeDesktopEncoderCapabilities(caps DesktopEncoderCapabilities) DesktopEncoderCapabilities {
encoders := make([]string, 0)
hardware := false
for _, profile := range caps.Profiles {
for _, provider := range profile.Providers {
encoders = appendDesktopUniqueString(encoders, provider)
if provider != "" && provider != "Software H.264 / JPEG" {
hardware = true
}
}
}
caps.Codecs = []DesktopCodecCapability{
{Codec: "jpeg", Encoders: []string{"Software JPEG"}, Transports: []string{"websocket"}},
{Codec: "raw", Encoders: []string{"Uncompressed"}, Transports: []string{"websocket"}},
}
if hevcAvailable() {
caps.Codecs = append([]DesktopCodecCapability{{
Codec: "hevc", Encoders: []string{"NVIDIA NVENC"},
Transports: []string{"websocket"}, Hardware: true,
}}, caps.Codecs...)
}
if h264Available() {
if len(encoders) == 0 {
encoders = []string{"Software H.264"}
}
caps.Codecs = append([]DesktopCodecCapability{{
Codec: "h264", Encoders: encoders,
Transports: []string{"websocket", "webrtc"}, Hardware: hardware,
}}, caps.Codecs...)
}
return caps
}
func appendDesktopUniqueString(values []string, value string) []string {
for _, existing := range values {
if existing == value {
return values
}
}
return append(values, value)
}
@@ -3,12 +3,12 @@
package capture
func ProbeDesktopEncoderCapabilities(display int) DesktopEncoderCapabilities {
return DesktopEncoderCapabilities{
return completeDesktopEncoderCapabilities(DesktopEncoderCapabilities{
Display: display,
Profiles: []DesktopEncoderProfile{
{MaxHeight: 720, Width: 1280, Height: 720, FPS: 60, Label: "60 FPS - 720p", Providers: []string{"Software H.264 / JPEG"}},
{MaxHeight: 1080, Width: 1920, Height: 1080, FPS: 60, Label: "60 FPS - 1080p", Providers: []string{"Software H.264 / JPEG"}},
},
Detail: "Hardware H.264 capability probing is available on Windows; safe software profiles are shown.",
}
})
}
@@ -0,0 +1,46 @@
package capture
import "testing"
func TestCompleteDesktopEncoderCapabilitiesAdvertisesUsableCodecs(t *testing.T) {
caps := completeDesktopEncoderCapabilities(DesktopEncoderCapabilities{
Profiles: []DesktopEncoderProfile{{
MaxHeight: 1080,
Width: 1920,
Height: 1080,
FPS: 60,
Providers: []string{"Software H.264 / JPEG"},
}},
})
byName := make(map[string]DesktopCodecCapability)
for _, codec := range caps.Codecs {
byName[codec.Codec] = codec
}
if _, ok := byName["jpeg"]; !ok {
t.Fatal("expected JPEG capability")
}
if _, ok := byName["raw"]; !ok {
t.Fatal("expected raw capability")
}
if h264Available() {
h264, ok := byName["h264"]
if !ok {
t.Fatal("expected H.264 capability when the encoder is available")
}
if len(h264.Transports) != 2 || h264.Transports[0] != "websocket" || h264.Transports[1] != "webrtc" {
t.Fatalf("unexpected H.264 transports: %v", h264.Transports)
}
}
if hevcAvailable() {
hevc, ok := byName["hevc"]
if !ok {
t.Fatal("expected HEVC capability when NVENC HEVC is available")
}
if len(hevc.Transports) != 1 || hevc.Transports[0] != "websocket" || !hevc.Hardware {
t.Fatalf("unexpected HEVC capability: %+v", hevc)
}
} else if _, ok := byName["hevc"]; ok {
t.Fatal("must not advertise HEVC when NVENC HEVC is unavailable")
}
}
@@ -44,6 +44,7 @@ func ProbeDesktopEncoderCapabilities(display int) DesktopEncoderCapabilities {
return cached.caps
}
caps := probeDesktopEncoderCapabilities(display, bounds)
caps = completeDesktopEncoderCapabilities(caps)
desktopCapabilityCache.entries[cacheKey] = desktopCapabilityCacheEntry{at: time.Now(), caps: caps}
for key, entry := range desktopCapabilityCache.entries {
if time.Since(entry.at) >= desktopCapabilityCacheTTL {
@@ -10,11 +10,21 @@ import (
)
func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Duration, bool, error) {
if blockCodec() != "h264" || useDesktopSoftwareH264() || !useDesktopDuplication() {
codec := blockCodec()
if (codec != "h264" && codec != "hevc") || (codec == "h264" && useDesktopSoftwareH264()) || !useDesktopDuplication() {
return wire.Frame{}, 0, 0, false, nil
}
forceKeyframe := webrtcpub.ConsumeKeyframeRequest()
data, width, height, captureDur, encodeDur, used, err := captureDisplayDXGIH264(display, forceKeyframe)
var data []byte
var width, height int
var captureDur, encodeDur time.Duration
var used bool
var err error
if codec == "hevc" {
data, width, height, captureDur, encodeDur, used, err = captureDisplayDXGIHEVC(display, forceKeyframe)
} else {
data, width, height, captureDur, encodeDur, used, err = captureDisplayDXGIH264(display, forceKeyframe)
}
if !used || err != nil {
return wire.Frame{}, captureDur, encodeDur, used, err
}
@@ -24,5 +34,5 @@ func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Durat
now := time.Now()
lastKeyframe.Store(now.UnixNano())
statFullFrames.Add(1)
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", Width: width, Height: height}, Data: data}, captureDur, encodeDur, true, nil
return wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: codec, Width: width, Height: height}, Data: data}, captureDur, encodeDur, true, nil
}
@@ -0,0 +1,86 @@
package capture
import (
"image"
"testing"
)
func TestHEVCEncoderProducesAnnexBHeadersAndKeyframe(t *testing.T) {
if !hevcAvailable() {
t.Skipf("HEVC encoder unavailable: %s", hevcAvailabilityDetail())
}
resetHEVCEncoder()
t.Cleanup(resetHEVCEncoder)
frame := image.NewRGBA(image.Rect(0, 0, 1280, 720))
encoded, err := encodeHEVCFrame(frame)
if err != nil {
t.Fatalf("encode HEVC frame: %v", err)
}
if len(encoded) == 0 {
t.Fatal("HEVC encoder returned an empty frame")
}
nalTypes := hevcAnnexBNALTypes(encoded)
for _, required := range []byte{32, 33, 34} {
if !containsByte(nalTypes, required) {
t.Fatalf("HEVC frame missing NAL type %d; got %v", required, nalTypes)
}
}
hasIntra := false
for _, nalType := range nalTypes {
if nalType >= 16 && nalType <= 21 {
hasIntra = true
break
}
}
if !hasIntra {
t.Fatalf("HEVC first frame is not an IRAP keyframe; got NAL types %v", nalTypes)
}
}
func TestHEVCD3D11TexturePipeline(t *testing.T) {
if !hevcAvailable() {
t.Skipf("HEVC encoder unavailable: %s", hevcAvailabilityDetail())
}
result := RunNVENCD3D11Smoke(NVENCD3D11SmokeOptions{
Width: 1280, Height: 720, FPS: 60, Frames: 4, Codec: "hevc",
})
if !result.OK {
t.Fatalf("HEVC D3D11 texture pipeline failed: %+v", result)
}
if result.TotalBytes == 0 {
t.Fatal("HEVC D3D11 texture pipeline produced no output")
}
t.Logf("HEVC D3D11 texture pipeline: first=%.2fms avg=%.2fms bytes=%d", result.FirstMS, result.AvgMS, result.TotalBytes)
}
func hevcAnnexBNALTypes(data []byte) []byte {
types := make([]byte, 0, 8)
for i := 0; i+4 < len(data); i++ {
startCodeLen := 0
if data[i] == 0 && data[i+1] == 0 && data[i+2] == 1 {
startCodeLen = 3
} else if i+4 < len(data) && data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1 {
startCodeLen = 4
}
if startCodeLen == 0 {
continue
}
nalIndex := i + startCodeLen
if nalIndex < len(data) {
types = append(types, (data[nalIndex]>>1)&0x3f)
i = nalIndex
}
}
return types
}
func containsByte(values []byte, wanted byte) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}
@@ -0,0 +1,18 @@
//go:build !windows
package capture
import (
"errors"
"image"
)
func encodeHEVCFrame(_ *image.RGBA) ([]byte, error) {
return nil, errors.New("HEVC NVENC is only available on Windows")
}
func hevcAvailable() bool { return false }
func hevcAvailabilityDetail() string { return "HEVC NVENC is only available on Windows" }
func resetHEVCEncoder() {}
@@ -0,0 +1,68 @@
//go:build windows
package capture
import (
"errors"
"fmt"
"image"
"sync"
)
var (
hevcMu sync.Mutex
hevcEnc h264FrameEncoder
hevcProbeOnce sync.Once
hevcProbeOK bool
hevcProbeDetail string
)
func encodeHEVCFrame(img *image.RGBA) ([]byte, error) {
if img == nil {
return nil, errors.New("nil hevc frame")
}
b := img.Bounds()
width, height := b.Dx(), b.Dy()
if width <= 0 || height <= 0 || width%2 != 0 || height%2 != 0 {
return nil, fmt.Errorf("hevc frame size must be positive and even, got %dx%d", width, height)
}
hevcMu.Lock()
defer hevcMu.Unlock()
fps := activeH264FPS()
if hevcEnc == nil || !hevcEnc.Matches(width, height, fps) {
closeH264Encoder(&hevcEnc)
enc, err := newNativeHEVCEncoder("desktop", width, height, fps)
if err != nil {
return nil, err
}
hevcEnc = enc
}
return hevcEnc.Encode(img)
}
func hevcAvailable() bool {
hevcProbeOnce.Do(func() {
enc, err := newNativeHEVCEncoder("probe", 1280, 720, 30)
if err != nil {
hevcProbeDetail = err.Error()
return
}
enc.Close()
hevcProbeOK = true
hevcProbeDetail = "NVIDIA NVENC HEVC Main profile"
})
return hevcProbeOK
}
func hevcAvailabilityDetail() string {
hevcAvailable()
return hevcProbeDetail
}
func resetHEVCEncoder() {
hevcMu.Lock()
defer hevcMu.Unlock()
closeH264Encoder(&hevcEnc)
resetNativeHEVCD3D11TextureEncoder()
}
@@ -9,6 +9,10 @@ func newNativeH264Encoder(stream string, width, height, fps int) (h264FrameEncod
return nil, fmt.Errorf("native NVENC D3D11 requires cgo for %dx%d@%dfps", width, height, fps)
}
func newNativeHEVCEncoder(stream string, width, height, fps int) (h264FrameEncoder, error) {
return nil, fmt.Errorf("native NVENC HEVC requires cgo for %dx%d@%dfps", width, height, fps)
}
func nativeH264AvailabilityDetail() string {
return ""
}
@@ -20,3 +24,11 @@ func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, in
func requestNativeH264D3D11TextureKeyframe() {}
func resetNativeH264D3D11TextureEncoder() {}
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
return nil, fmt.Errorf("native NVENC HEVC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
}
func requestNativeHEVCD3D11TextureKeyframe() {}
func resetNativeHEVCD3D11TextureEncoder() {}
@@ -9,11 +9,12 @@ import (
)
type NVENCD3D11SmokeOptions struct {
Width int `json:"width"`
Height int `json:"height"`
FPS int `json:"fps"`
Frames int `json:"frames"`
Bitrate int `json:"bitrate"`
Width int `json:"width"`
Height int `json:"height"`
FPS int `json:"fps"`
Frames int `json:"frames"`
Bitrate int `json:"bitrate"`
Codec string `json:"codec,omitempty"`
}
type NVENCD3D11SmokeResult struct {
@@ -134,6 +134,16 @@ static void nvenc_configure_h264_stream(NV_ENC_CONFIG *cfg, int fps) {
cfg->encodeCodecConfig.h264Config.h264VUIParameters.colourMatrix = NV_ENC_VUI_MATRIX_COEFFS_BT709;
}
static void nvenc_configure_hevc_stream(NV_ENC_CONFIG *cfg, int fps) {
(void)fps;
cfg->gopLength = NVENC_INFINITE_GOPLENGTH;
cfg->frameIntervalP = 1;
cfg->encodeCodecConfig.hevcConfig.idrPeriod = NVENC_INFINITE_GOPLENGTH;
cfg->encodeCodecConfig.hevcConfig.repeatSPSPPS = 1;
cfg->encodeCodecConfig.hevcConfig.level = NV_ENC_LEVEL_HEVC_52;
cfg->encodeCodecConfig.hevcConfig.chromaFormatIDC = 1;
}
static void nvenc_set_video_processor_color_space(nvenc_d3d11_texture_encoder *enc) {
D3D11_VIDEO_PROCESSOR_COLOR_SPACE input_space;
memset(&input_space, 0, sizeof(input_space));
@@ -287,7 +297,7 @@ static void nvenc_release_texture_encoder(nvenc_d3d11_texture_encoder *enc) {
free(enc);
}
static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D11Device *device, int input_width, int input_height, int encode_width, int encode_height, int fps, int bitrate, NV_ENC_BUFFER_FORMAT buffer_format, int dxgi_format, int output_mode) {
static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D11Device *device, int input_width, int input_height, int encode_width, int encode_height, int fps, int bitrate, NV_ENC_BUFFER_FORMAT buffer_format, int dxgi_format, int output_mode, int codec) {
nvenc_d3d11_texture_create_result out;
memset(&out, 0, sizeof(out));
if (!device) {
@@ -374,7 +384,8 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D
memset(&preset, 0, sizeof(preset));
preset.version = NV_ENC_PRESET_CONFIG_VER;
preset.presetCfg.version = NV_ENC_CONFIG_VER;
status = enc->api.nvEncGetEncodePresetConfigEx(enc->encoder, NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_P1_GUID, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, &preset);
GUID codec_guid = codec == 1 ? NV_ENC_CODEC_HEVC_GUID : NV_ENC_CODEC_H264_GUID;
status = enc->api.nvEncGetEncodePresetConfigEx(enc->encoder, codec_guid, NV_ENC_PRESET_P1_GUID, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, &preset);
if (status != NV_ENC_SUCCESS) {
out.stage = 9;
out.nvstatus = status;
@@ -382,18 +393,22 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D
nvenc_release_texture_encoder(enc);
return out;
}
preset.presetCfg.profileGUID = NV_ENC_H264_PROFILE_HIGH_GUID;
preset.presetCfg.profileGUID = codec == 1 ? NV_ENC_HEVC_PROFILE_MAIN_GUID : NV_ENC_H264_PROFILE_HIGH_GUID;
preset.presetCfg.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;
preset.presetCfg.rcParams.averageBitRate = (uint32_t)bitrate;
preset.presetCfg.rcParams.maxBitRate = (uint32_t)bitrate;
preset.presetCfg.rcParams.vbvBufferSize = (uint32_t)(bitrate / fps);
preset.presetCfg.rcParams.vbvInitialDelay = (uint32_t)(bitrate / fps);
nvenc_configure_h264_stream(&preset.presetCfg, fps);
if (codec == 1) {
nvenc_configure_hevc_stream(&preset.presetCfg, fps);
} else {
nvenc_configure_h264_stream(&preset.presetCfg, fps);
}
NV_ENC_INITIALIZE_PARAMS init;
memset(&init, 0, sizeof(init));
init.version = NV_ENC_INITIALIZE_PARAMS_VER;
init.encodeGUID = NV_ENC_CODEC_H264_GUID;
init.encodeGUID = codec_guid;
init.presetGUID = NV_ENC_PRESET_P1_GUID;
init.encodeWidth = (uint32_t)encode_width;
init.encodeHeight = (uint32_t)encode_height;
@@ -594,13 +609,13 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D
return out;
}
static nvenc_d3d11_create_result nvenc_create_d3d11_encoder(int width, int height, int fps, int bitrate) {
static nvenc_d3d11_create_result nvenc_create_d3d11_encoder(int width, int height, int fps, int bitrate, int codec) {
nvenc_d3d11_create_result out;
memset(&out, 0, sizeof(out));
if (width <= 0 || height <= 0 || (width & 1) || (height & 1) || fps <= 0) {
if (width <= 0 || height <= 0 || (width & 1) || (height & 1) || fps <= 0 || (codec != 0 && codec != 1)) {
out.stage = 1;
out.nvstatus = NV_ENC_ERR_INVALID_PARAM;
strncpy(out.message, "width/height/fps must be positive, and width/height must be even", sizeof(out.message)-1);
strncpy(out.message, "invalid width/height/fps or codec", sizeof(out.message)-1);
return out;
}
@@ -679,7 +694,8 @@ static nvenc_d3d11_create_result nvenc_create_d3d11_encoder(int width, int heigh
memset(&preset, 0, sizeof(preset));
preset.version = NV_ENC_PRESET_CONFIG_VER;
preset.presetCfg.version = NV_ENC_CONFIG_VER;
status = enc->api.nvEncGetEncodePresetConfigEx(enc->encoder, NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_P1_GUID, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, &preset);
GUID codec_guid = codec == 1 ? NV_ENC_CODEC_HEVC_GUID : NV_ENC_CODEC_H264_GUID;
status = enc->api.nvEncGetEncodePresetConfigEx(enc->encoder, codec_guid, NV_ENC_PRESET_P1_GUID, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, &preset);
if (status != NV_ENC_SUCCESS) {
out.stage = 8;
out.nvstatus = status;
@@ -687,18 +703,22 @@ static nvenc_d3d11_create_result nvenc_create_d3d11_encoder(int width, int heigh
nvenc_release_encoder(enc);
return out;
}
preset.presetCfg.profileGUID = NV_ENC_H264_PROFILE_HIGH_GUID;
preset.presetCfg.profileGUID = codec == 1 ? NV_ENC_HEVC_PROFILE_MAIN_GUID : NV_ENC_H264_PROFILE_HIGH_GUID;
preset.presetCfg.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;
preset.presetCfg.rcParams.averageBitRate = (uint32_t)bitrate;
preset.presetCfg.rcParams.maxBitRate = (uint32_t)bitrate;
preset.presetCfg.rcParams.vbvBufferSize = (uint32_t)(bitrate / fps);
preset.presetCfg.rcParams.vbvInitialDelay = (uint32_t)(bitrate / fps);
nvenc_configure_h264_stream(&preset.presetCfg, fps);
if (codec == 1) {
nvenc_configure_hevc_stream(&preset.presetCfg, fps);
} else {
nvenc_configure_h264_stream(&preset.presetCfg, fps);
}
NV_ENC_INITIALIZE_PARAMS init;
memset(&init, 0, sizeof(init));
init.version = NV_ENC_INITIALIZE_PARAMS_VER;
init.encodeGUID = NV_ENC_CODEC_H264_GUID;
init.encodeGUID = codec_guid;
init.presetGUID = NV_ENC_PRESET_P1_GUID;
init.encodeWidth = (uint32_t)width;
init.encodeHeight = (uint32_t)height;
@@ -1325,7 +1345,7 @@ static nvenc_d3d11_smoke_result run_nvenc_d3d11_smoke_c(int width, int height, i
return out;
}
static nvenc_d3d11_smoke_result run_nvenc_d3d11_texture_pipeline_smoke_c(int width, int height, int fps, int frames, int bitrate) {
static nvenc_d3d11_smoke_result run_nvenc_d3d11_texture_pipeline_smoke_c(int width, int height, int fps, int frames, int bitrate, int codec) {
nvenc_d3d11_smoke_result out;
memset(&out, 0, sizeof(out));
out.width = width;
@@ -1391,7 +1411,7 @@ static nvenc_d3d11_smoke_result run_nvenc_d3d11_texture_pipeline_smoke_c(int wid
return out;
}
nvenc_d3d11_texture_create_result created = nvenc_create_d3d11_texture_encoder(device, width, height, width, height, fps, bitrate, NV_ENC_BUFFER_FORMAT_ARGB, DXGI_FORMAT_B8G8R8A8_UNORM, 0);
nvenc_d3d11_texture_create_result created = nvenc_create_d3d11_texture_encoder(device, width, height, width, height, fps, bitrate, NV_ENC_BUFFER_FORMAT_ARGB, DXGI_FORMAT_B8G8R8A8_UNORM, 0, codec);
if (!created.encoder) {
out.stage = 5;
out.hr = created.hr;
@@ -1467,11 +1487,12 @@ import (
)
type NVENCD3D11SmokeOptions struct {
Width int `json:"width"`
Height int `json:"height"`
FPS int `json:"fps"`
Frames int `json:"frames"`
Bitrate int `json:"bitrate"`
Width int `json:"width"`
Height int `json:"height"`
FPS int `json:"fps"`
Frames int `json:"frames"`
Bitrate int `json:"bitrate"`
Codec string `json:"codec,omitempty"`
}
type NVENCD3D11SmokeResult struct {
@@ -1505,8 +1526,15 @@ func RunNVENCD3D11Smoke(opts NVENCD3D11SmokeOptions) NVENCD3D11SmokeResult {
}
if opts.Bitrate <= 0 {
opts.Bitrate = targetH264Bitrate(opts.Width, opts.Height, opts.FPS)
if strings.EqualFold(opts.Codec, "hevc") || strings.EqualFold(opts.Codec, "h265") {
opts.Bitrate = opts.Bitrate * 7 / 10
}
}
raw := C.run_nvenc_d3d11_texture_pipeline_smoke_c(C.int(opts.Width), C.int(opts.Height), C.int(opts.FPS), C.int(opts.Frames), C.int(opts.Bitrate))
codec := 0
if strings.EqualFold(opts.Codec, "hevc") || strings.EqualFold(opts.Codec, "h265") {
codec = 1
}
raw := C.run_nvenc_d3d11_texture_pipeline_smoke_c(C.int(opts.Width), C.int(opts.Height), C.int(opts.FPS), C.int(opts.Frames), C.int(opts.Bitrate), C.int(codec))
result := NVENCD3D11SmokeResult{
OK: raw.ok != 0,
Width: int(raw.width),
@@ -1545,7 +1573,7 @@ type nativeH264Encoder struct {
func newNativeH264Encoder(stream string, width, height, fps int) (h264FrameEncoder, error) {
bitrate := targetH264Bitrate(width, height, fps)
raw := C.nvenc_create_d3d11_encoder(C.int(width), C.int(height), C.int(fps), C.int(bitrate))
raw := C.nvenc_create_d3d11_encoder(C.int(width), C.int(height), C.int(fps), C.int(bitrate), 0)
if raw.encoder == nil {
return nil, formatNVENCCreateError(raw)
}
@@ -1560,6 +1588,23 @@ func newNativeH264Encoder(stream string, width, height, fps int) (h264FrameEncod
return enc, nil
}
func newNativeHEVCEncoder(stream string, width, height, fps int) (h264FrameEncoder, error) {
bitrate := targetH264Bitrate(width, height, fps) * 7 / 10
raw := C.nvenc_create_d3d11_encoder(C.int(width), C.int(height), C.int(fps), C.int(bitrate), 1)
if raw.encoder == nil {
return nil, formatNVENCCreateError(raw)
}
enc := &nativeH264Encoder{
enc: raw.encoder,
width: width,
height: height,
requestedFPS: fps,
fps: fps,
}
log.Printf("capture: native NVENC D3D11 hevc encoder active stream=%s provider=NVIDIA NVENC size=%dx%d fps=%d input=NV12(upload) bitrate=%d", stream, width, height, fps, bitrate)
return enc, nil
}
func nativeH264AvailabilityDetail() string {
probe := RunNVENCSmoke()
if probe.Available {
@@ -1645,15 +1690,26 @@ type nativeD3D11TextureH264Encoder struct {
outputName string
frame uint64
slowLogs int
codec string
}
func newNativeD3D11TextureH264Encoder(device unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32) (*nativeD3D11TextureH264Encoder, error) {
return newNativeD3D11TextureEncoder(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat, "h264")
}
func newNativeD3D11TextureEncoder(device unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, codec string) (*nativeD3D11TextureH264Encoder, error) {
bufferFormat, name, ok := nvencBufferFormatForDXGI(dxgiFormat)
if !ok {
return nil, fmt.Errorf("unsupported DXGI texture format %d", dxgiFormat)
}
bitrate := targetH264Bitrate(encodeWidth, encodeHeight, fps)
modes := preferredNVENCD3D11OutputModes()
codecID := 0
if codec == "hevc" {
codecID = 1
bitrate = bitrate * 7 / 10
modes = []int{0}
}
var lastErr error
var raw C.nvenc_d3d11_texture_create_result
outputMode := 0
@@ -1670,6 +1726,7 @@ func newNativeD3D11TextureH264Encoder(device unsafe.Pointer, inputWidth, inputHe
C.NV_ENC_BUFFER_FORMAT(bufferFormat),
C.int(dxgiFormat),
C.int(mode),
C.int(codecID),
)
if raw.encoder != nil {
outputMode = mode
@@ -1703,11 +1760,12 @@ func newNativeD3D11TextureH264Encoder(device unsafe.Pointer, inputWidth, inputHe
bufferFormat: uint32(bufferFormat),
outputMode: outputMode,
outputName: outputName,
codec: codec,
}
if inputWidth != encodeWidth || inputHeight != encodeHeight {
log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC input=%dx%d output=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", inputWidth, inputHeight, encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate)
log.Printf("capture: native NVENC D3D11 desktop texture encoder active codec=%s provider=NVIDIA NVENC input=%dx%d output=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", codec, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate)
} else {
log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC size=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate)
log.Printf("capture: native NVENC D3D11 desktop texture encoder active codec=%s provider=NVIDIA NVENC size=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", codec, encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate)
}
return enc, nil
}
@@ -1867,6 +1925,9 @@ var (
nativeTextureH264Mu sync.Mutex
nativeTextureH264Enc *nativeD3D11TextureH264Encoder
nativeTextureH264ForceIDR atomic.Bool
nativeTextureHEVCMu sync.Mutex
nativeTextureHEVCEnc *nativeD3D11TextureH264Encoder
nativeTextureHEVCForceIDR atomic.Bool
)
func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
@@ -1903,3 +1964,38 @@ func resetNativeH264D3D11TextureEncoder() {
}
nativeTextureH264ForceIDR.Store(false)
}
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
if device == nil || texture == nil {
return nil, fmt.Errorf("nil D3D11 device or texture")
}
nativeTextureHEVCMu.Lock()
defer nativeTextureHEVCMu.Unlock()
if nativeTextureHEVCEnc == nil || !nativeTextureHEVCEnc.Matches(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat) {
if nativeTextureHEVCEnc != nil {
nativeTextureHEVCEnc.Close()
nativeTextureHEVCEnc = nil
}
enc, err := newNativeD3D11TextureEncoder(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat, "hevc")
if err != nil {
return nil, err
}
nativeTextureHEVCEnc = enc
}
return nativeTextureHEVCEnc.EncodeTexture(texture, forceIDR || nativeTextureHEVCForceIDR.Swap(false))
}
func requestNativeHEVCD3D11TextureKeyframe() {
nativeTextureHEVCForceIDR.Store(true)
}
func resetNativeHEVCD3D11TextureEncoder() {
nativeTextureHEVCMu.Lock()
defer nativeTextureHEVCMu.Unlock()
if nativeTextureHEVCEnc != nil {
nativeTextureHEVCEnc.Close()
nativeTextureHEVCEnc = nil
}
nativeTextureHEVCForceIDR.Store(false)
}
@@ -661,7 +661,11 @@ func captureDisplayDXGI(display int) (*image.RGBA, error) {
}
func captureDisplayDXGIH264(display int, forceKeyframe bool) ([]byte, int, int, time.Duration, time.Duration, bool, error) {
return dxgiState.captureH264(display, forceKeyframe)
return dxgiState.captureVideo(display, "h264", forceKeyframe)
}
func captureDisplayDXGIHEVC(display int, forceKeyframe bool) ([]byte, int, int, time.Duration, time.Duration, bool, error) {
return dxgiState.captureVideo(display, "hevc", forceKeyframe)
}
func (s *duplicationState) reset() {
@@ -685,6 +689,7 @@ func (s *duplicationState) closeLocked() {
s.h264LastDesc = d3d11Texture2DDesc{}
}
resetH264D3D11TextureEncoder()
resetNativeHEVCD3D11TextureEncoder()
if s.context != nil {
s.context.Release()
s.context = nil
@@ -875,13 +880,13 @@ func (s *duplicationState) copyLastBaseLocked(src *image.RGBA) {
s.lastBase = cloneRGBA(src)
}
func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, int, int, time.Duration, time.Duration, bool, error) {
func (s *duplicationState) captureVideo(display int, codec string, forceKeyframe bool) ([]byte, int, int, time.Duration, time.Duration, bool, error) {
capStart := time.Now()
s.mu.Lock()
defer s.mu.Unlock()
if !useDesktopDuplication() || cursorCaptureEnabled.Load() {
return nil, 0, 0, 0, 0, false, errors.New("direct h264 requires DXGI duplication and cursor capture disabled")
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct %s requires DXGI duplication and cursor capture disabled", codec)
}
if err := s.ensure(display); err != nil {
s.lastFail = time.Now()
@@ -891,10 +896,10 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
width := int(s.desc.ModeDesc.Width)
height := int(s.desc.ModeDesc.Height)
if width <= 0 || height <= 0 || width%2 != 0 || height%2 != 0 {
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 requires positive even dimensions, got %dx%d", width, height)
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct %s requires positive even dimensions, got %dx%d", codec, width, height)
}
if s.desc.Rotation != dxgiModeRotationIdentity && s.desc.Rotation != dxgiModeRotationUnspecified {
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 requires identity rotation, got %d", s.desc.Rotation)
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct %s requires identity rotation, got %d", codec, s.desc.Rotation)
}
encodeW, encodeH := width, height
if scale := effectiveScale(width, height); scale != 1 {
@@ -906,7 +911,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 computed invalid scaled output %dx%d", encodeW, encodeH)
}
directH264ScaleOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 scaling enabled scale=%.2f input=%dx%d output=%dx%d", scale, width, height, encodeW, encodeH)
log.Printf("capture: direct DXGI hardware %s scaling enabled scale=%.2f input=%dx%d output=%dx%d", codec, scale, width, height, encodeW, encodeH)
})
}
@@ -918,7 +923,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
captureDur := time.Since(capStart)
fps := activeH264FPS()
encStart := time.Now()
out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
out, _, err := encodeDesktopD3D11Texture(codec, h264D3D11TextureRequest{
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH,
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: forceKeyframe,
@@ -926,7 +931,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
encodeDur := time.Since(encStart)
if err != nil {
directH264WarnOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 cached-frame encode failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
log.Printf("capture: direct DXGI hardware %s cached-frame encode failed for %dx%d@%dfps format=%d: %v; using readback path", codec, width, height, fps, s.h264LastDesc.Format, err)
})
return nil, 0, 0, 0, 0, false, err
}
@@ -978,13 +983,13 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
}
if srcDesc.Format != dxgiFormatB8G8R8A8UNorm && srcDesc.Format != dxgiFormatR8G8B8A8UNorm {
directH264WarnOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 unavailable: unsupported DXGI texture format %d; using readback path", srcDesc.Format)
log.Printf("capture: direct DXGI hardware %s unavailable: unsupported DXGI texture format %d; using readback path", codec, srcDesc.Format)
})
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 unsupported DXGI texture format %d", srcDesc.Format)
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct %s unsupported DXGI texture format %d", codec, srcDesc.Format)
}
if err := s.cacheH264TextureLocked(tex, srcDesc); err != nil {
directH264WarnOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 cache setup failed for %dx%d format=%d: %v; using readback path", width, height, srcDesc.Format, err)
log.Printf("capture: direct DXGI hardware %s cache setup failed for %dx%d format=%d: %v; using readback path", codec, width, height, srcDesc.Format, err)
})
return nil, 0, 0, 0, 0, false, err
}
@@ -995,7 +1000,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
fps := activeH264FPS()
encStart := time.Now()
out, provider, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
out, provider, err := encodeDesktopD3D11Texture(codec, h264D3D11TextureRequest{
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH,
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: forceKeyframe,
@@ -1003,7 +1008,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
encodeDur := time.Since(encStart)
if err != nil {
directH264WarnOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
log.Printf("capture: direct DXGI hardware %s failed for %dx%d@%dfps format=%d: %v; using readback path", codec, width, height, fps, s.h264LastDesc.Format, err)
})
return nil, 0, 0, 0, 0, false, err
}
@@ -1011,11 +1016,20 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
return nil, encodeW, encodeH, captureDur, encodeDur, true, nil
}
directH264ActiveOnce.Do(func() {
log.Printf("capture: direct DXGI hardware h264 path active provider=%q input=%dx%d output=%dx%d fps=%d dxgi_format=%d", provider, width, height, encodeW, encodeH, fps, srcDesc.Format)
log.Printf("capture: direct DXGI hardware %s path active provider=%q input=%dx%d output=%dx%d fps=%d dxgi_format=%d", codec, provider, width, height, encodeW, encodeH, fps, srcDesc.Format)
})
return out, encodeW, encodeH, captureDur, encodeDur, true, nil
}
func encodeDesktopD3D11Texture(codec string, req h264D3D11TextureRequest) ([]byte, string, error) {
if codec == "hevc" {
out, err := encodeNativeHEVCD3D11Texture(req.Device, req.Texture, req.InputWidth, req.InputHeight,
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
return out, "NVIDIA NVENC", err
}
return encodeH264D3D11Texture(req)
}
func (s *duplicationState) cacheH264TextureLocked(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {
if src == nil || s.device == nil || s.context == nil {
return errors.New("nil D3D11 source/device/context")