//go:build cgo package capture import ( "bytes" "fmt" "image" "sync" "sync/atomic" x264 "github.com/gen2brain/x264-go" ) 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 ) func encodeH264Frame(img *image.RGBA) ([]byte, error) { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() h264Mu.Lock() defer h264Mu.Unlock() if err := ensureH264EncoderLocked(width, height); err != nil { h264LastErr = err return nil, err } h264Buf.Reset() if err := h264Enc.Encode(img); err != nil { h264LastErr = err return nil, err } h264LastErr = nil n := h264Buf.Len() if n == 0 { return nil, nil } if cap(h264Scratch) < n { h264Scratch = make([]byte, n, n+(n/4)) } else { h264Scratch = h264Scratch[:n] } copy(h264Scratch, h264Buf.Bytes()) return h264Scratch, nil } func h264Available() bool { return true } func h264AvailabilityDetail() string { h264Mu.Lock() defer h264Mu.Unlock() if h264LastErr != nil { return fmt.Sprintf("x264 runtime error: %v", h264LastErr) } 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) { if fps < 1 { fps = 1 } h264TargetFPS.Store(int64(fps)) } func activeH264FPS() int { if v := int(h264TargetFPS.Load()); v > 0 { return v } return 60 // sensible default for desktop streaming } func ensureH264EncoderLocked(width, height int) error { fps := activeH264FPS() if h264Enc != nil && h264Width == width && h264Height == height && h264FPS == fps { return nil } closeH264EncoderLocked() opts := &x264.Options{ Width: width, Height: height, FrameRate: fps, Tune: "zerolatency", Preset: "veryfast", Profile: "main", RateControl: "crf", RateConstant: 23, LogLevel: x264.LogError, } enc, err := x264.NewEncoder(&h264Buf, opts) if err != nil { return err } h264Enc = enc h264Width = width h264Height = height h264FPS = fps return nil } func resetH264Encoder() { h264Mu.Lock() defer h264Mu.Unlock() closeH264EncoderLocked() } func closeH264EncoderLocked() { if h264Enc != nil { _ = h264Enc.Close() h264Enc = nil } h264Width = 0 h264Height = 0 h264FPS = 0 h264Buf.Reset() } var ( hvncH264Mu sync.Mutex hvncH264Enc *x264.Encoder hvncH264Buf bytes.Buffer hvncH264Width int hvncH264Height int hvncH264FPS int hvncH264Scratch []byte ) func encodeH264FrameHVNC(img *image.RGBA) ([]byte, error) { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() hvncH264Mu.Lock() defer hvncH264Mu.Unlock() if err := ensureHVNCH264EncoderLocked(width, height); err != nil { return nil, err } hvncH264Buf.Reset() if err := hvncH264Enc.Encode(img); err != nil { return nil, err } n := hvncH264Buf.Len() if n == 0 { return nil, nil } if cap(hvncH264Scratch) < n { hvncH264Scratch = make([]byte, n, n+(n/4)) } else { hvncH264Scratch = hvncH264Scratch[:n] } copy(hvncH264Scratch, hvncH264Buf.Bytes()) return hvncH264Scratch, nil } func resetH264EncoderHVNC() { hvncH264Mu.Lock() defer hvncH264Mu.Unlock() closeHVNCH264EncoderLocked() } func ensureHVNCH264EncoderLocked(width, height int) error { fps := activeH264FPS() if hvncH264Enc != nil && hvncH264Width == width && hvncH264Height == height && hvncH264FPS == fps { return nil } closeHVNCH264EncoderLocked() opts := &x264.Options{ Width: width, Height: height, FrameRate: fps, Tune: "zerolatency", Preset: "veryfast", Profile: "main", RateControl: "crf", RateConstant: 23, LogLevel: x264.LogError, } enc, err := x264.NewEncoder(&hvncH264Buf, opts) if err != nil { return err } hvncH264Enc = enc hvncH264Width = width hvncH264Height = height hvncH264FPS = fps return nil } func closeHVNCH264EncoderLocked() { if hvncH264Enc != nil { _ = hvncH264Enc.Close() hvncH264Enc = nil } hvncH264Width = 0 hvncH264Height = 0 hvncH264FPS = 0 hvncH264Buf.Reset() }