start of fix

This commit is contained in:
KingKDot
2026-07-21 16:08:45 -05:00
parent 3a05497aa3
commit 6926522cc2
24 changed files with 669 additions and 211 deletions
+117 -54
View File
@@ -110,17 +110,17 @@ 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 video capture failed: %v (sending black frame, consecutive=%d)", err, consecutiveCaptureFails.Load())
return sendBlackFrame(ctx, env)
}
emitDesktopCursor(ctx, env, display, frame.Header.Width, frame.Header.Height)
if len(frame.Data) == 0 {
suppressDesktopHardwareH264(err)
log.Printf("capture: direct video encode failed: %v; falling back to the software H.264 path", err)
} else {
emitDesktopCursor(ctx, env, display, frame.Header.Width, frame.Header.Height)
if len(frame.Data) == 0 {
consecutiveCaptureFails.Store(0)
return nil
}
consecutiveCaptureFails.Store(0)
return nil
return sendCompletedFrame(ctx, env, frame, display, t0, captureDur, encodeDur)
}
consecutiveCaptureFails.Store(0)
return sendCompletedFrame(ctx, env, frame, display, t0, captureDur, encodeDur)
}
img, err := safeCaptureDisplay(display)
@@ -145,7 +145,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
consecutiveCaptureFails.Store(0)
captureDur := time.Since(t0)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindDesktop)
willSendViaWebRTC := desktopCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindDesktop)
var slotAcquired bool
if !willSendViaWebRTC {
if !AcquireFrameSlot() {
@@ -157,7 +157,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
frameWidth := img.Bounds().Dx()
frameHeight := img.Bounds().Dy()
quality := jpegQuality()
quality := desktopJPEGQuality()
frame, encodeDur, err := buildFrame(img, display, quality)
PutRGBA(img)
img = nil
@@ -413,7 +413,7 @@ func captureAllDisplaysAndSend(ctx context.Context, env *rt.Env) error {
PutRGBA(part.img)
}
quality := jpegQuality()
quality := desktopJPEGQuality()
frame, _, err := buildFrame(canvas, 0, quality)
canvasW, canvasH := canvas.Rect.Dx(), canvas.Rect.Dy()
PutRGBA(canvas)
@@ -482,6 +482,8 @@ func MonitorCount() int {
const frameLogInterval = 5 * time.Second
const desktopHardwareH264FallbackCooldown = 30 * time.Second
var consecutiveCaptureFails atomic.Int64
var (
@@ -511,12 +513,16 @@ var (
statBlockFallbacks atomic.Int64
statFrameSlotSkips atomic.Int64
overrideQuality atomic.Int64
overrideCodec atomic.Value
desktopSoftwareH264 atomic.Bool
h264WarnOnce sync.Once
hevcWarnOnce sync.Once
codecLogOnce sync.Once
desktopOverrideQuality atomic.Int64
backstageOverrideQuality atomic.Int64
desktopOverrideCodec atomic.Value
backstageOverrideCodec atomic.Value
desktopSoftwareH264 atomic.Bool
desktopHardwareFallbackUntil atomic.Int64
h264WarnOnce sync.Once
backstageH264WarnOnce sync.Once
hevcWarnOnce sync.Once
codecLogOnce sync.Once
prevMu sync.Mutex
prevFrame *prevImage
@@ -645,7 +651,7 @@ func ResetPrevbackstage() {
func RequestDesktopFullFrame() {
requestFullFrames(2)
webrtcpub.RequestKeyframe()
webrtcpub.RequestKeyframe(webrtcpub.KindDesktop)
RequestDesktopH264Keyframe()
}
@@ -653,13 +659,27 @@ func SetDesktopSoftwareH264(enabled bool) {
if desktopSoftwareH264.Swap(enabled) == enabled {
return
}
desktopHardwareFallbackUntil.Store(0)
resetH264Encoder()
requestFullFrames(2)
log.Printf("capture: desktop software h264 %v", enabled)
}
func useDesktopSoftwareH264() bool {
return desktopSoftwareH264.Load()
if desktopSoftwareH264.Load() {
return true
}
until := desktopHardwareFallbackUntil.Load()
return until > 0 && time.Now().UnixNano() < until
}
func suppressDesktopHardwareH264(err error) {
until := time.Now().Add(desktopHardwareH264FallbackCooldown).UnixNano()
previous := desktopHardwareFallbackUntil.Swap(until)
resetH264Encoder()
if previous < time.Now().UnixNano() {
log.Printf("capture: hardware H.264 temporarily unavailable (%v); using software H.264 for %s", err, desktopHardwareH264FallbackCooldown)
}
}
func RequestbackstageFullFrame() {
@@ -667,12 +687,19 @@ func RequestbackstageFullFrame() {
backstagePrevFrame = nil
backstagePrevMu.Unlock()
backstageLastKeyframe.Store(0)
resetH264Encoderbackstage()
RequestBackstageH264Keyframe()
}
func jpegQuality() int {
func desktopJPEGQuality() int {
return streamJPEGQuality(&desktopOverrideQuality)
}
if q := overrideQuality.Load(); q > 0 {
func backstageJPEGQuality() int {
return streamJPEGQuality(&backstageOverrideQuality)
}
func streamJPEGQuality(override *atomic.Int64) int {
if q := override.Load(); q > 0 {
return int(q)
}
q := int(loadOnceInt(&cachedJPEGQuality, 95))
@@ -761,13 +788,13 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
height := img.Bounds().Dy()
full := false
now := time.Now()
codec := blockCodec()
codec := desktopCodec()
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() {
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
resetHEVCEncoder()
}
hevcBytes, err := encodeHEVCFrame(img)
@@ -782,10 +809,10 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
resetHEVCEncoder()
if h264Available() {
codec = "h264"
overrideCodec.Store("h264")
desktopOverrideCodec.Store("h264")
} else {
codec = "jpeg"
overrideCodec.Store("jpeg")
desktopOverrideCodec.Store("jpeg")
}
}
}
@@ -795,7 +822,7 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
log.Printf("capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
codec = "jpeg"
} else {
if webrtcpub.ConsumeKeyframeRequest() {
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
resetH264Encoder()
}
h264Bytes, err := encodeH264Frame(img)
@@ -892,7 +919,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
encStart := time.Now()
width := img.Bounds().Dx()
height := img.Bounds().Dy()
codec := blockCodec()
codec := backstageCodec()
now := time.Now()
if codec == "h264" {
@@ -900,7 +927,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
log.Printf("backstage capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
codec = "jpeg"
} else {
if webrtcpub.ConsumeKeyframeRequest() {
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.Kindbackstage) {
resetH264Encoderbackstage()
}
h264Bytes, err := encodeH264Framebackstage(img)
@@ -910,7 +937,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", Backstage: true}, Data: h264Bytes}
return frame, time.Since(encStart), nil
}
h264WarnOnce.Do(func() {
backstageH264WarnOnce.Do(func() {
detail := h264AvailabilityDetail()
if detail != "" {
log.Printf("backstage capture: h264 encode unavailable, falling back to jpeg: %v (%s)", err, detail)
@@ -1589,12 +1616,7 @@ func encodeBlockRaw(img *image.RGBA, x, y, w, h int) []byte {
return buf
}
func blockCodec() string {
if v := overrideCodec.Load(); v != nil {
if s, ok := v.(string); ok && s != "" {
return s
}
}
func defaultCodec() string {
blockCodecOnce.Do(func() {
codec := strings.ToLower(os.Getenv(blockCodecEnv))
switch codec {
@@ -1613,12 +1635,33 @@ func blockCodec() string {
return cachedBlockCodec
}
func SetQualityAndCodec(quality int, codec string) {
func selectedCodec(override *atomic.Value) string {
if v := override.Load(); v != nil {
if s, ok := v.(string); ok && s != "" {
return s
}
}
return defaultCodec()
}
func desktopCodec() string {
return selectedCodec(&desktopOverrideCodec)
}
func backstageCodec() string {
codec := selectedCodec(&backstageOverrideCodec)
if codec == "hevc" {
return "jpeg"
}
return codec
}
func SetDesktopQualityAndCodec(quality int, codec string) {
if quality > 0 {
if quality > 100 {
quality = 100
}
overrideQuality.Store(int64(quality))
desktopOverrideQuality.Store(int64(quality))
}
s := strings.ToLower(strings.TrimSpace(codec))
if s == "h265" {
@@ -1630,40 +1673,60 @@ func SetQualityAndCodec(quality int, codec string) {
if h264Available() {
fallback = "h264"
}
log.Printf("capture: requested codec=hevc but unavailable (%s); forcing codec=%s", detail, fallback)
log.Printf("capture: requested desktop codec=hevc but unavailable (%s); forcing codec=%s", detail, fallback)
s = fallback
}
if s == "h264" && !h264Available() {
detail := h264AvailabilityDetail()
if detail != "" {
log.Printf("capture: requested codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
} else {
log.Printf("capture: requested codec=h264 but unavailable; forcing codec=jpeg")
}
log.Printf("capture: requested desktop codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
s = "jpeg"
}
switch s {
case "raw", "rgba", "jpeg", "h264", "hevc":
overrideCodec.Store(s)
desktopOverrideCodec.Store(s)
h264WarnOnce = sync.Once{}
hevcWarnOnce = sync.Once{}
if s != "h264" {
resetH264Encoder()
resetH264Encoderbackstage()
}
if s != "hevc" {
resetHEVCEncoder()
}
case "":
default:
overrideCodec.Store("jpeg")
desktopOverrideCodec.Store("jpeg")
resetH264Encoder()
resetH264Encoderbackstage()
resetHEVCEncoder()
}
}
func SetBackstageQualityAndCodec(quality int, codec string) {
if quality > 0 {
if quality > 100 {
quality = 100
}
backstageOverrideQuality.Store(int64(quality))
}
s := strings.ToLower(strings.TrimSpace(codec))
if s == "h264" && !h264Available() {
detail := h264AvailabilityDetail()
log.Printf("capture: requested backstage codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
s = "jpeg"
}
switch s {
case "raw", "rgba", "jpeg", "h264":
backstageOverrideCodec.Store(s)
backstageH264WarnOnce = sync.Once{}
if s != "h264" {
resetH264Encoderbackstage()
}
case "":
default:
backstageOverrideCodec.Store("jpeg")
resetH264Encoderbackstage()
}
}
func Nowbackstage(ctx context.Context, env *rt.Env) error {
if env.Cfg.DisableCapture {
return sendBlackFramebackstage(ctx, env)
@@ -1725,7 +1788,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
}
captureDur := time.Since(t0)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
willSendViaWebRTC := backstageCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
var slotAcquired bool
if !willSendViaWebRTC && !AcquireFrameSlot() {
PutRGBA(img)
@@ -1733,7 +1796,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
}
slotAcquired = !willSendViaWebRTC
quality := jpegQuality()
quality := backstageJPEGQuality()
frame, encodeDur, err := buildFramebackstage(img, 0, quality)
PutRGBA(img)
img = nil
@@ -1873,7 +1936,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
}
captureDur := time.Since(t0)
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
willSendViaWebRTC := backstageCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
var slotAcquired bool
if !willSendViaWebRTC && !AcquireFrameSlot() {
PutRGBA(img)
@@ -1881,7 +1944,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
}
slotAcquired = !willSendViaWebRTC
quality := jpegQuality()
quality := backstageJPEGQuality()
frame, encodeDur, err := buildFramebackstage(img, display, quality)
PutRGBA(img)
img = nil
@@ -39,7 +39,7 @@ func RunCaptureSmoke(opts CaptureSmokeOptions) []CaptureSmokeResult {
opts.Display = 0
}
if opts.FPS > 0 {
SetH264TargetFPS(opts.FPS)
SetDesktopH264TargetFPS(opts.FPS)
}
if opts.MaxHeight != 0 {
SetMaxResolution(opts.MaxHeight)
@@ -144,7 +144,7 @@ func TestActualScreenCapture(t *testing.T) {
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
}
quality := jpegQuality()
quality := desktopJPEGQuality()
frame, encodeDur, err := buildFrame(img, 0, quality)
if err != nil {
t.Errorf("Failed to build frame from captured image: %v", err)
@@ -270,7 +270,7 @@ func TestFrameFPS(t *testing.T) {
}
func TestJPEGQuality(t *testing.T) {
quality := jpegQuality()
quality := desktopJPEGQuality()
if quality < 1 || quality > 100 {
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
}
@@ -479,11 +479,11 @@ func TestSetFrameFlowTargetFPSEnvOverride(t *testing.T) {
}
func TestRequestDesktopFullFrameRequestsWebRTCKeyframe(t *testing.T) {
_ = webrtcpub.ConsumeKeyframeRequest()
_ = webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop)
RequestDesktopFullFrame()
if !webrtcpub.ConsumeKeyframeRequest() {
if !webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
t.Fatal("expected desktop full-frame request to force the next WebRTC video frame to be a keyframe")
}
}
@@ -3,5 +3,5 @@
package capture
func resetH264TextureEncoderForBitrate() {
resetH264D3D11TextureEncoder()
resetAllH264D3D11TextureEncoders()
}
@@ -1,6 +1,8 @@
package capture
import (
"errors"
"fmt"
"image"
"image/color"
"os"
@@ -12,14 +14,17 @@ import (
func resetCodecSelectionForTest() {
blockCodecOnce = sync.Once{}
cachedBlockCodec = ""
overrideCodec.Store("")
desktopOverrideCodec.Store("")
backstageOverrideCodec.Store("")
desktopOverrideQuality.Store(0)
backstageOverrideQuality.Store(0)
}
func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
t.Cleanup(resetCodecSelectionForTest)
SetQualityAndCodec(80, "h264")
got := blockCodec()
SetDesktopQualityAndCodec(80, "h264")
got := desktopCodec()
if h264Available() {
if got != "h264" {
@@ -36,13 +41,56 @@ func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
func TestSetQualityAndCodec_InvalidCodecForcesJpeg(t *testing.T) {
t.Cleanup(resetCodecSelectionForTest)
SetQualityAndCodec(75, "invalid-codec")
got := blockCodec()
SetDesktopQualityAndCodec(75, "invalid-codec")
got := desktopCodec()
if got != "jpeg" {
t.Fatalf("expected invalid codec to force jpeg, got %q", got)
}
}
func TestDesktopAndBackstageCodecSelectionsAreIndependent(t *testing.T) {
t.Cleanup(resetCodecSelectionForTest)
SetDesktopQualityAndCodec(80, "h264")
SetBackstageQualityAndCodec(65, "jpeg")
wantDesktop := "jpeg"
if h264Available() {
wantDesktop = "h264"
}
if got := desktopCodec(); got != wantDesktop {
t.Fatalf("desktop codec = %q, want %q after backstage update", got, wantDesktop)
}
if got := backstageCodec(); got != "jpeg" {
t.Fatalf("backstage codec = %q, want jpeg", got)
}
if got := desktopJPEGQuality(); got != 80 {
t.Fatalf("desktop quality = %d, want 80", got)
}
if got := backstageJPEGQuality(); got != 65 {
t.Fatalf("backstage quality = %d, want 65", got)
}
}
func TestDesktopHardwareFailureDoesNotChangeBackstageCodec(t *testing.T) {
t.Cleanup(func() {
desktopHardwareFallbackUntil.Store(0)
resetCodecSelectionForTest()
})
SetDesktopQualityAndCodec(80, "h264")
SetBackstageQualityAndCodec(65, "h264")
before := backstageCodec()
suppressDesktopHardwareH264(errors.New("NVENC session failure"))
if !useDesktopSoftwareH264() {
t.Fatal("desktop did not enter temporary software H.264 fallback")
}
if got := backstageCodec(); got != before {
t.Fatalf("backstage codec changed from %q to %q after desktop encoder failure", before, got)
}
}
func TestH264AvailabilityDetail_NotEmpty(t *testing.T) {
detail := strings.TrimSpace(h264AvailabilityDetail())
if detail == "" {
@@ -93,6 +141,59 @@ func TestEncodeH264Frame_WhenAvailableProducesBytes(t *testing.T) {
}
}
func TestDesktopAndBackstageH264EncodeConcurrently(t *testing.T) {
if !h264Available() {
t.Skip("h264 is unavailable in this build")
}
SetDesktopH264TargetFPS(60)
SetBackstageH264TargetFPS(30)
t.Cleanup(func() {
resetH264Encoder()
resetH264Encoderbackstage()
})
const width, height = 640, 360
start := make(chan struct{})
results := make(chan error, 2)
run := func(name string, encode func(*image.RGBA) ([]byte, error), seed uint8) {
<-start
img := image.NewRGBA(image.Rect(0, 0, width, height))
produced := false
for frame := range 12 {
for y := range height {
for x := range width {
img.SetRGBA(x, y, color.RGBA{
R: uint8(x*3+frame*7) + seed,
G: uint8(y*3+frame*5) + seed,
B: uint8(frame*11) + seed,
A: 255,
})
}
}
out, err := encode(img)
if err != nil {
results <- fmt.Errorf("%s encode frame %d: %w", name, frame, err)
return
}
produced = produced || len(out) > 0
}
if !produced {
results <- fmt.Errorf("%s produced no H.264 output", name)
return
}
results <- nil
}
go run("desktop", encodeH264Frame, 17)
go run("backstage", encodeH264Framebackstage, 83)
close(start)
for range 2 {
if err := <-results; err != nil {
t.Fatal(err)
}
}
}
func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
prev := os.Getenv(blockCodecEnv)
if err := os.Setenv(blockCodecEnv, "raw"); err != nil {
@@ -104,7 +205,7 @@ func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
})
resetCodecSelectionForTest()
got := blockCodec()
got := desktopCodec()
if got != "raw" {
t.Fatalf("expected env codec raw, got %q", got)
}
@@ -17,13 +17,13 @@ func directVideoKeyframeDue(requested bool, nowNs, lastKeyframeNs int64) bool {
}
func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Duration, bool, error) {
codec := blockCodec()
codec := desktopCodec()
if (codec != "h264" && codec != "hevc") || (codec == "h264" && useDesktopSoftwareH264()) || !useDesktopDuplication() {
return wire.Frame{}, 0, 0, false, nil
}
now := time.Now()
forceKeyframe := directVideoKeyframeDue(
webrtcpub.ConsumeKeyframeRequest(),
webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop),
now.UnixNano(),
lastKeyframe.Load(),
)
@@ -23,7 +23,8 @@ func resetH264Encoder() {}
func RequestDesktopH264Keyframe() {}
func resetH264Encoderbackstage() {}
func resetH264Encoderbackstage() {}
func RequestBackstageH264Keyframe() {}
func encodeH264Framebackstage(_ *image.RGBA) ([]byte, error) {
return nil, errors.New("h264 support not available (cgo disabled)")
@@ -33,4 +34,6 @@ func encodeH264FrameWebcam(_ *image.RGBA) ([]byte, error) {
return nil, errors.New("h264 support not available (cgo disabled)")
}
func SetH264TargetFPS(_ int) {}
func SetDesktopH264TargetFPS(_ int) {}
func SetBackstageH264TargetFPS(_ int) {}
func SetWebcamH264TargetFPS(_ int) {}
@@ -519,7 +519,9 @@ type mfH264Candidate struct {
}
var (
h264TargetFPS atomic.Int64
desktopH264TargetFPS atomic.Int64
backstageH264TargetFPS atomic.Int64
webcamH264TargetFPS atomic.Int64
h264Mu sync.Mutex
h264Enc h264FrameEncoder
@@ -565,7 +567,7 @@ func encodeH264FrameWithEncoder(slot *h264FrameEncoder, stream string, img *imag
return nil, fmt.Errorf("h264 frame size must be even, got %dx%d", width, height)
}
fps := activeH264FPS()
fps := activeH264FPSForStream(stream)
if *slot == nil || !(*slot).Matches(width, height, fps) {
if *slot != nil {
(*slot).Close()
@@ -577,7 +579,39 @@ func encodeH264FrameWithEncoder(slot *h264FrameEncoder, stream string, img *imag
}
*slot = enc
}
return (*slot).Encode(img)
return encodeH264FrameWithRuntimeFallback(slot, stream, img, fps, newSoftwareH264Fallback)
}
type h264SoftwareFallbackFactory func(width, height, fps int) (h264FrameEncoder, error)
func newSoftwareH264Fallback(width, height, fps int) (h264FrameEncoder, error) {
return newMFSoftwareH264Encoder(width, height, fps)
}
func encodeH264FrameWithRuntimeFallback(
slot *h264FrameEncoder,
stream string,
img *image.RGBA,
fps int,
createSoftware h264SoftwareFallbackFactory,
) ([]byte, error) {
out, err := (*slot).Encode(img)
if err == nil {
return out, nil
}
hardwareErr := err
closeH264Encoder(slot)
fallback, fallbackErr := createSoftware(img.Bounds().Dx(), img.Bounds().Dy(), fps)
if fallbackErr != nil {
return nil, fmt.Errorf("%s h264 encoder failed (%v) and software fallback could not start: %w", stream, hardwareErr, fallbackErr)
}
*slot = fallback
log.Printf("capture: %s h264 encoder failed at runtime: %v; switched this stream to Media Foundation software H.264", stream, hardwareErr)
out, err = fallback.Encode(img)
if err != nil {
return nil, fmt.Errorf("%s software h264 fallback encode failed after hardware error %v: %w", stream, hardwareErr, err)
}
return out, nil
}
func h264Available() bool {
@@ -601,18 +635,45 @@ func h264AvailabilityDetail() string {
return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + detail + ")"
}
func SetH264TargetFPS(fps int) {
func SetDesktopH264TargetFPS(fps int) {
setH264TargetFPS(&desktopH264TargetFPS, fps)
}
func SetBackstageH264TargetFPS(fps int) {
setH264TargetFPS(&backstageH264TargetFPS, fps)
}
func SetWebcamH264TargetFPS(fps int) {
setH264TargetFPS(&webcamH264TargetFPS, fps)
}
func setH264TargetFPS(target *atomic.Int64, fps int) {
if fps < 1 {
fps = 1
}
h264TargetFPS.Store(int64(fps))
target.Store(int64(fps))
}
func activeH264FPS() int {
if v := int(h264TargetFPS.Load()); v > 0 {
return activeH264FPSValue(&desktopH264TargetFPS, 60)
}
func activeH264FPSForStream(stream string) int {
switch stream {
case "backstage":
return activeH264FPSValue(&backstageH264TargetFPS, 60)
case "webcam":
return activeH264FPSValue(&webcamH264TargetFPS, 30)
default:
return activeH264FPS()
}
}
func activeH264FPSValue(target *atomic.Int64, fallback int) int {
if v := int(target.Load()); v > 0 {
return v
}
return 60
return fallback
}
func resetH264Encoder() {
@@ -622,7 +683,7 @@ func resetH264Encoder() {
}
func RequestDesktopH264Keyframe() {
requestH264D3D11TextureKeyframe()
requestH264D3D11TextureKeyframe("desktop")
resetH264Encoder()
}
@@ -632,6 +693,11 @@ func resetH264Encoderbackstage() {
closeH264Encoder(&backstageH264Enc)
}
func RequestBackstageH264Keyframe() {
requestH264D3D11TextureKeyframe("backstage")
resetH264Encoderbackstage()
}
func closeH264Encoder(slot *h264FrameEncoder) {
if slot != nil && *slot != nil {
(*slot).Close()
@@ -13,15 +13,17 @@ import (
)
var (
h264Mu sync.Mutex
h264Enc *x264.Encoder
h264Buf bytes.Buffer
h264Width int
h264Height int
h264FPS int
h264LastErr error
h264Scratch []byte
h264TargetFPS atomic.Int64 // set by the stream handler; default 60
h264Mu sync.Mutex
h264Enc *x264.Encoder
h264Buf bytes.Buffer
h264Width int
h264Height int
h264FPS int
h264LastErr error
h264Scratch []byte
desktopH264TargetFPS atomic.Int64
backstageH264TargetFPS atomic.Int64
webcamH264TargetFPS atomic.Int64
)
func encodeH264Frame(img *image.RGBA) ([]byte, error) {
@@ -70,22 +72,34 @@ func h264AvailabilityDetail() string {
return "cgo build with x264-go"
}
// SetH264TargetFPS sets the frame rate hint for the x264 encoder so its
// rate-control matches the actual capture cadence. Call this before (or
// when) streaming starts. The encoder is automatically re-created on the
// next frame if the value differs from the current one.
func SetH264TargetFPS(fps int) {
func SetDesktopH264TargetFPS(fps int) {
setH264TargetFPS(&desktopH264TargetFPS, fps)
}
func SetBackstageH264TargetFPS(fps int) {
setH264TargetFPS(&backstageH264TargetFPS, fps)
}
func SetWebcamH264TargetFPS(fps int) {
setH264TargetFPS(&webcamH264TargetFPS, fps)
}
func setH264TargetFPS(target *atomic.Int64, fps int) {
if fps < 1 {
fps = 1
}
h264TargetFPS.Store(int64(fps))
target.Store(int64(fps))
}
func activeH264FPS() int {
if v := int(h264TargetFPS.Load()); v > 0 {
return activeH264FPSValue(&desktopH264TargetFPS, 60)
}
func activeH264FPSValue(target *atomic.Int64, fallback int) int {
if v := int(target.Load()); v > 0 {
return v
}
return 60 // sensible default for desktop streaming
return fallback
}
func ensureH264EncoderLocked(width, height int) error {
@@ -187,8 +201,12 @@ func resetH264Encoderbackstage() {
closebackstageH264EncoderLocked()
}
func RequestBackstageH264Keyframe() {
resetH264Encoderbackstage()
}
func ensurebackstageH264EncoderLocked(width, height int) error {
fps := activeH264FPS()
fps := activeH264FPSValue(&backstageH264TargetFPS, 60)
if backstageH264Enc != nil && backstageH264Width == width && backstageH264Height == height && backstageH264FPS == fps {
return nil
}
@@ -267,7 +285,7 @@ func encodeH264FrameWebcam(img *image.RGBA) ([]byte, error) {
}
func ensureWebcamH264EncoderLocked(width, height int) error {
fps := activeH264FPS()
fps := activeH264FPSValue(&webcamH264TargetFPS, 30)
if webcamH264Enc != nil && webcamH264Width == width && webcamH264Height == height && webcamH264FPS == fps {
return nil
}
@@ -43,90 +43,107 @@ type h264D3D11TextureBackend interface {
Reset()
}
type nvencD3D11TextureBackend struct{}
type nvencD3D11TextureBackend struct {
stream string
}
func (nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" }
func (*nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" }
func (nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
return encodeNativeH264D3D11Texture(req.Device, req.Texture, req.InputWidth, req.InputHeight,
func (b *nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
return encodeNativeH264D3D11Texture(b.stream, req.Device, req.Texture, req.InputWidth, req.InputHeight,
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
}
func (nvencD3D11TextureBackend) RequestKeyframe() {
requestNativeH264D3D11TextureKeyframe()
func (b *nvencD3D11TextureBackend) RequestKeyframe() {
requestNativeH264D3D11TextureKeyframe(b.stream)
}
func (nvencD3D11TextureBackend) Reset() {
resetNativeH264D3D11TextureEncoder()
func (b *nvencD3D11TextureBackend) Reset() {
resetNativeH264D3D11TextureEncoder(b.stream)
}
var d3d11H264TextureRegistry = struct {
type h264D3D11TextureRegistry struct {
sync.Mutex
backends []h264D3D11TextureBackend
active h264D3D11TextureBackend
failures map[d3d11H264FailureKey]d3d11H264Failure
}{
backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}},
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
}
func init() {
func newH264D3D11TextureRegistry(stream string) *h264D3D11TextureRegistry {
registry := &h264D3D11TextureRegistry{
backends: []h264D3D11TextureBackend{&nvencD3D11TextureBackend{stream: stream}},
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
}
if backend := newQSVD3D11TextureBackend(); backend != nil {
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
registry.backends = append(registry.backends, backend)
}
if backend := newAMFD3D11TextureBackend(); backend != nil {
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
registry.backends = append(registry.backends, backend)
}
return registry
}
func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) {
var (
d3d11H264TextureRegistry = newH264D3D11TextureRegistry("desktop")
backstageD3D11H264TextureRegistry = newH264D3D11TextureRegistry("backstage")
)
func h264D3D11Registry(stream string) *h264D3D11TextureRegistry {
if stream == "backstage" {
return backstageD3D11H264TextureRegistry
}
return d3d11H264TextureRegistry
}
func encodeH264D3D11Texture(stream string, req h264D3D11TextureRequest) ([]byte, string, error) {
if req.Device == nil || req.Texture == nil {
return nil, "", fmt.Errorf("nil D3D11 device or texture")
}
d3d11H264TextureRegistry.Lock()
defer d3d11H264TextureRegistry.Unlock()
registry := h264D3D11Registry(stream)
registry.Lock()
defer registry.Unlock()
failedActiveName := ""
errorsByBackend := make([]string, 0, len(d3d11H264TextureRegistry.backends))
if active := d3d11H264TextureRegistry.active; active != nil {
errorsByBackend := make([]string, 0, len(registry.backends))
if active := registry.active; active != nil {
key := d3d11H264FailureKeyFor(active.Name(), req)
if failure, failed := activeD3D11H264Failure(key, time.Now()); !failed {
if failure, failed := registry.activeFailure(key, time.Now()); !failed {
out, err := active.Encode(req)
if err == nil {
delete(d3d11H264TextureRegistry.failures, key)
delete(registry.failures, key)
return out, active.Name(), nil
}
rememberD3D11H264Failure(key, err)
registry.rememberFailure(key, err)
active.Reset()
failedActiveName = active.Name()
errorsByBackend = append(errorsByBackend, active.Name()+": "+err.Error())
d3d11H264TextureRegistry.active = nil
registry.active = nil
} else {
errorsByBackend = append(errorsByBackend, active.Name()+": "+failure.err+" (cooldown)")
active.Reset()
failedActiveName = active.Name()
d3d11H264TextureRegistry.active = nil
registry.active = nil
}
}
now := time.Now()
for _, backend := range d3d11H264TextureRegistry.backends {
for _, backend := range registry.backends {
if backend == nil || (failedActiveName != "" && backend.Name() == failedActiveName) {
continue
}
key := d3d11H264FailureKeyFor(backend.Name(), req)
if failure, failed := activeD3D11H264Failure(key, now); failed {
if failure, failed := registry.activeFailure(key, now); failed {
errorsByBackend = append(errorsByBackend, backend.Name()+": "+failure.err+" (cooldown)")
continue
}
out, err := backend.Encode(req)
if err == nil {
delete(d3d11H264TextureRegistry.failures, key)
d3d11H264TextureRegistry.active = backend
delete(registry.failures, key)
registry.active = backend
return out, backend.Name(), nil
}
backend.Reset()
rememberD3D11H264Failure(key, err)
registry.rememberFailure(key, err)
errorsByBackend = append(errorsByBackend, backend.Name()+": "+err.Error())
}
if len(errorsByBackend) == 0 {
@@ -142,51 +159,58 @@ func d3d11H264FailureKeyFor(backend string, req h264D3D11TextureRequest) d3d11H2
}
}
func activeD3D11H264Failure(key d3d11H264FailureKey, now time.Time) (d3d11H264Failure, bool) {
failure, ok := d3d11H264TextureRegistry.failures[key]
func (r *h264D3D11TextureRegistry) activeFailure(key d3d11H264FailureKey, now time.Time) (d3d11H264Failure, bool) {
failure, ok := r.failures[key]
if !ok {
return d3d11H264Failure{}, false
}
if !now.Before(failure.until) {
delete(d3d11H264TextureRegistry.failures, key)
delete(r.failures, key)
return d3d11H264Failure{}, false
}
return failure, true
}
func rememberD3D11H264Failure(key d3d11H264FailureKey, err error) {
if len(d3d11H264TextureRegistry.failures) > 128 {
func (r *h264D3D11TextureRegistry) rememberFailure(key d3d11H264FailureKey, err error) {
if len(r.failures) > 128 {
now := time.Now()
for existingKey, failure := range d3d11H264TextureRegistry.failures {
for existingKey, failure := range r.failures {
if !now.Before(failure.until) {
delete(d3d11H264TextureRegistry.failures, existingKey)
delete(r.failures, existingKey)
}
}
}
d3d11H264TextureRegistry.failures[key] = d3d11H264Failure{until: time.Now().Add(d3d11H264FailureCooldown), err: err.Error()}
r.failures[key] = d3d11H264Failure{until: time.Now().Add(d3d11H264FailureCooldown), err: err.Error()}
}
func requestH264D3D11TextureKeyframe() {
d3d11H264TextureRegistry.Lock()
defer d3d11H264TextureRegistry.Unlock()
if d3d11H264TextureRegistry.active != nil {
d3d11H264TextureRegistry.active.RequestKeyframe()
func requestH264D3D11TextureKeyframe(stream string) {
registry := h264D3D11Registry(stream)
registry.Lock()
defer registry.Unlock()
if registry.active != nil {
registry.active.RequestKeyframe()
return
}
for _, backend := range d3d11H264TextureRegistry.backends {
for _, backend := range registry.backends {
if backend != nil {
backend.RequestKeyframe()
}
}
}
func resetH264D3D11TextureEncoder() {
d3d11H264TextureRegistry.Lock()
defer d3d11H264TextureRegistry.Unlock()
for _, backend := range d3d11H264TextureRegistry.backends {
func resetH264D3D11TextureEncoder(stream string) {
registry := h264D3D11Registry(stream)
registry.Lock()
defer registry.Unlock()
for _, backend := range registry.backends {
if backend != nil {
backend.Reset()
}
}
d3d11H264TextureRegistry.active = nil
registry.active = nil
}
func resetAllH264D3D11TextureEncoders() {
resetH264D3D11TextureEncoder("desktop")
resetH264D3D11TextureEncoder("backstage")
}
@@ -4,6 +4,7 @@ package capture
import (
"errors"
"image"
"testing"
"unsafe"
)
@@ -54,7 +55,7 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
device, texture := 1, 2
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture)}
out, provider, err := encodeH264D3D11Texture(req)
out, provider, err := encodeH264D3D11Texture("desktop", req)
if err != nil {
t.Fatalf("first encode: %v", err)
}
@@ -65,7 +66,7 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
t.Fatalf("unexpected calls first.reset=%d second.encode=%d", first.resetCalls, second.encodeCalls)
}
_, provider, err = encodeH264D3D11Texture(req)
_, provider, err = encodeH264D3D11Texture("desktop", req)
if err != nil || provider != "second" {
t.Fatalf("active backend reuse provider=%q err=%v", provider, err)
}
@@ -73,11 +74,11 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
t.Fatalf("active backend was not reused: first=%d second=%d", first.encodeCalls, second.encodeCalls)
}
requestH264D3D11TextureKeyframe()
requestH264D3D11TextureKeyframe("desktop")
if second.keyframeCalls != 1 || first.keyframeCalls != 0 {
t.Fatalf("keyframe routed incorrectly: first=%d second=%d", first.keyframeCalls, second.keyframeCalls)
}
resetH264D3D11TextureEncoder()
resetH264D3D11TextureEncoder("desktop")
if first.resetCalls != 2 || second.resetCalls != 1 {
t.Fatalf("reset did not reach every backend: first=%d second=%d", first.resetCalls, second.resetCalls)
}
@@ -105,13 +106,129 @@ func TestD3D11TextureBackendFailuresUseCooldown(t *testing.T) {
device, texture := 1, 2
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture), EncodeWidth: 2560, EncodeHeight: 1440, FPS: 240}
if _, _, err := encodeH264D3D11Texture(req); err == nil {
if _, _, err := encodeH264D3D11Texture("desktop", req); err == nil {
t.Fatal("expected initial profile failure")
}
if _, _, err := encodeH264D3D11Texture(req); err == nil {
if _, _, err := encodeH264D3D11Texture("desktop", req); err == nil {
t.Fatal("expected cached profile failure")
}
if first.encodeCalls != 1 || second.encodeCalls != 1 {
t.Fatalf("failed profile was retried during cooldown: first=%d second=%d", first.encodeCalls, second.encodeCalls)
}
}
type fakeH264FrameEncoder struct {
output []byte
err error
encodeCall int
closed bool
}
func (e *fakeH264FrameEncoder) Encode(*image.RGBA) ([]byte, error) {
e.encodeCall++
return e.output, e.err
}
func (e *fakeH264FrameEncoder) Close() {
e.closed = true
}
func (e *fakeH264FrameEncoder) Matches(_, _, _ int) bool {
return true
}
func TestH264RuntimeFailureFallsBackPerStream(t *testing.T) {
primary := &fakeH264FrameEncoder{err: errors.New("NVENC session failed")}
software := &fakeH264FrameEncoder{output: []byte{7, 8, 9}}
var slot h264FrameEncoder = primary
factoryCalls := 0
factory := func(width, height, fps int) (h264FrameEncoder, error) {
factoryCalls++
if width != 64 || height != 32 || fps != 47 {
t.Fatalf("fallback config = %dx%d@%d, want 64x32@47", width, height, fps)
}
return software, nil
}
out, err := encodeH264FrameWithRuntimeFallback(
&slot,
"backstage",
image.NewRGBA(image.Rect(0, 0, 64, 32)),
47,
factory,
)
if err != nil {
t.Fatalf("runtime fallback failed: %v", err)
}
if factoryCalls != 1 || primary.encodeCall != 1 || !primary.closed {
t.Fatalf("primary cleanup/fallback calls = factory:%d encode:%d closed:%v", factoryCalls, primary.encodeCall, primary.closed)
}
if slot != software || software.encodeCall != 1 {
t.Fatalf("software encoder was not retained after fallback")
}
if len(out) != 3 || out[0] != 7 {
t.Fatalf("fallback output = %v", out)
}
}
func TestH264TargetFPSIsIndependentPerStream(t *testing.T) {
SetDesktopH264TargetFPS(120)
SetBackstageH264TargetFPS(37)
SetWebcamH264TargetFPS(24)
if got := activeH264FPS(); got != 120 {
t.Fatalf("desktop fps = %d, want 120", got)
}
if got := activeH264FPSForStream("backstage"); got != 37 {
t.Fatalf("backstage fps = %d, want 37", got)
}
if got := activeH264FPSForStream("webcam"); got != 24 {
t.Fatalf("webcam fps = %d, want 24", got)
}
}
func TestD3D11TextureEncodersAreIsolatedByStream(t *testing.T) {
desktopBackend := &fakeD3D11TextureBackend{name: "desktop", outputs: [][]byte{{1}, {2}}}
backstageBackend := &fakeD3D11TextureBackend{name: "backstage", outputs: [][]byte{{3}}}
savedDesktop := d3d11H264TextureRegistry
savedBackstage := backstageD3D11H264TextureRegistry
d3d11H264TextureRegistry = &h264D3D11TextureRegistry{
backends: []h264D3D11TextureBackend{desktopBackend},
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
}
backstageD3D11H264TextureRegistry = &h264D3D11TextureRegistry{
backends: []h264D3D11TextureBackend{backstageBackend},
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
}
t.Cleanup(func() {
d3d11H264TextureRegistry = savedDesktop
backstageD3D11H264TextureRegistry = savedBackstage
})
desktopDevice, desktopTexture := 1, 2
backstageDevice, backstageTexture := 3, 4
desktopReq := h264D3D11TextureRequest{
Device: unsafe.Pointer(&desktopDevice), Texture: unsafe.Pointer(&desktopTexture),
EncodeWidth: 1920, EncodeHeight: 1080, FPS: 60,
}
backstageReq := h264D3D11TextureRequest{
Device: unsafe.Pointer(&backstageDevice), Texture: unsafe.Pointer(&backstageTexture),
EncodeWidth: 1280, EncodeHeight: 720, FPS: 30,
}
if _, _, err := encodeH264D3D11Texture("desktop", desktopReq); err != nil {
t.Fatalf("desktop encode: %v", err)
}
if _, _, err := encodeH264D3D11Texture("backstage", backstageReq); err != nil {
t.Fatalf("backstage encode: %v", err)
}
if _, _, err := encodeH264D3D11Texture("desktop", desktopReq); err != nil {
t.Fatalf("second desktop encode: %v", err)
}
if desktopBackend.encodeCalls != 2 || backstageBackend.encodeCalls != 1 {
t.Fatalf("stream calls crossed: desktop=%d backstage=%d", desktopBackend.encodeCalls, backstageBackend.encodeCalls)
}
if desktopBackend.resetCalls != 0 || backstageBackend.resetCalls != 0 {
t.Fatalf("one stream reset the other: desktop=%d backstage=%d", desktopBackend.resetCalls, backstageBackend.resetCalls)
}
}
@@ -17,13 +17,13 @@ func nativeH264AvailabilityDetail() string {
return ""
}
func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
func encodeNativeH264D3D11Texture(_ string, device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
return nil, fmt.Errorf("native NVENC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
}
func requestNativeH264D3D11TextureKeyframe() {}
func requestNativeH264D3D11TextureKeyframe(_ string) {}
func resetNativeH264D3D11TextureEncoder() {}
func resetNativeH264D3D11TextureEncoder(_ string) {}
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)
@@ -1921,48 +1921,62 @@ func envBool(name string) bool {
}
}
type nativeTextureH264StreamState struct {
sync.Mutex
encoder *nativeD3D11TextureH264Encoder
forceIDR atomic.Bool
}
var (
nativeTextureH264Mu sync.Mutex
nativeTextureH264Enc *nativeD3D11TextureH264Encoder
nativeTextureH264ForceIDR atomic.Bool
nativeTextureH264Streams sync.Map
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) {
func nativeTextureH264State(stream string) *nativeTextureH264StreamState {
if stream == "" {
stream = "desktop"
}
state, _ := nativeTextureH264Streams.LoadOrStore(stream, &nativeTextureH264StreamState{})
return state.(*nativeTextureH264StreamState)
}
func encodeNativeH264D3D11Texture(stream string, 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")
}
nativeTextureH264Mu.Lock()
defer nativeTextureH264Mu.Unlock()
state := nativeTextureH264State(stream)
state.Lock()
defer state.Unlock()
if nativeTextureH264Enc == nil || !nativeTextureH264Enc.Matches(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat) {
if nativeTextureH264Enc != nil {
nativeTextureH264Enc.Close()
nativeTextureH264Enc = nil
if state.encoder == nil || !state.encoder.Matches(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat) {
if state.encoder != nil {
state.encoder.Close()
state.encoder = nil
}
enc, err := newNativeD3D11TextureH264Encoder(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat)
if err != nil {
return nil, err
}
nativeTextureH264Enc = enc
state.encoder = enc
}
return nativeTextureH264Enc.EncodeTexture(texture, forceIDR || nativeTextureH264ForceIDR.Swap(false))
return state.encoder.EncodeTexture(texture, forceIDR || state.forceIDR.Swap(false))
}
func requestNativeH264D3D11TextureKeyframe() {
nativeTextureH264ForceIDR.Store(true)
func requestNativeH264D3D11TextureKeyframe(stream string) {
nativeTextureH264State(stream).forceIDR.Store(true)
}
func resetNativeH264D3D11TextureEncoder() {
nativeTextureH264Mu.Lock()
defer nativeTextureH264Mu.Unlock()
if nativeTextureH264Enc != nil {
nativeTextureH264Enc.Close()
nativeTextureH264Enc = nil
func resetNativeH264D3D11TextureEncoder(stream string) {
state := nativeTextureH264State(stream)
state.Lock()
defer state.Unlock()
if state.encoder != nil {
state.encoder.Close()
state.encoder = nil
}
nativeTextureH264ForceIDR.Store(false)
state.forceIDR.Store(false)
}
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
@@ -213,7 +213,7 @@ var (
)
func VirtualTryDirectH264Frame() (wire.Frame, time.Duration, time.Duration, bool, error) {
if blockCodec() != "h264" {
if backstageCodec() != "h264" {
return wire.Frame{}, 0, 0, false, nil
}
return virtualDupState.captureDirectH264()
@@ -280,12 +280,12 @@ func (s *virtualDuplicationState) captureDirectH264() (wire.Frame, time.Duration
}
func (s *virtualDuplicationState) encodeCachedDirectH264(capStart time.Time, width, height int) (wire.Frame, time.Duration, time.Duration, bool, error) {
fps := activeH264FPS()
fps := activeH264FPSForStream("backstage")
encStart := time.Now()
out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
out, _, err := encodeH264D3D11Texture("backstage", h264D3D11TextureRequest{
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
InputWidth: width, InputHeight: height, EncodeWidth: width, EncodeHeight: height,
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: webrtcpub.ConsumeKeyframeRequest(),
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: webrtcpub.ConsumeKeyframeRequest(webrtcpub.Kindbackstage),
})
encodeDur := time.Since(encStart)
if err != nil {
@@ -688,7 +688,7 @@ func (s *duplicationState) closeLocked() {
s.h264LastTex = nil
s.h264LastDesc = d3d11Texture2DDesc{}
}
resetH264D3D11TextureEncoder()
resetH264D3D11TextureEncoder("desktop")
resetNativeHEVCD3D11TextureEncoder()
if s.context != nil {
s.context.Release()
@@ -1027,7 +1027,7 @@ func encodeDesktopD3D11Texture(codec string, req h264D3D11TextureRequest) ([]byt
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
return out, "NVIDIA NVENC", err
}
return encodeH264D3D11Texture(req)
return encodeH264D3D11Texture("desktop", req)
}
func (s *duplicationState) cacheH264TextureLocked(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {