2026-06-06 16:08:28 -05:00
|
|
|
//go:build cgo && !windows
|
2026-03-05 14:45:15 -06:00
|
|
|
|
|
|
|
|
package capture
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-03-06 13:27:51 -06:00
|
|
|
"fmt"
|
2026-03-05 14:45:15 -06:00
|
|
|
"image"
|
|
|
|
|
"sync"
|
2026-03-13 09:28:16 -05:00
|
|
|
"sync/atomic"
|
2026-03-05 14:45:15 -06:00
|
|
|
|
|
|
|
|
x264 "github.com/gen2brain/x264-go"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
2026-07-21 16:08:45 -05:00
|
|
|
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
|
2026-03-05 14:45:15 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-03-06 13:27:51 -06:00
|
|
|
h264LastErr = err
|
2026-03-05 14:45:15 -06:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h264Buf.Reset()
|
|
|
|
|
if err := h264Enc.Encode(img); err != nil {
|
2026-03-06 13:27:51 -06:00
|
|
|
h264LastErr = err
|
2026-03-05 14:45:15 -06:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-03-06 13:27:51 -06:00
|
|
|
h264LastErr = nil
|
2026-03-05 14:45:15 -06:00
|
|
|
|
2026-03-13 09:28:16 -05:00
|
|
|
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
|
2026-03-05 14:45:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func h264Available() bool {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 13:27:51 -06:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 16:08:45 -05:00
|
|
|
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) {
|
2026-03-13 09:28:16 -05:00
|
|
|
if fps < 1 {
|
|
|
|
|
fps = 1
|
|
|
|
|
}
|
2026-07-21 16:08:45 -05:00
|
|
|
target.Store(int64(fps))
|
2026-03-13 09:28:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func activeH264FPS() int {
|
2026-07-21 16:08:45 -05:00
|
|
|
return activeH264FPSValue(&desktopH264TargetFPS, 60)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func activeH264FPSValue(target *atomic.Int64, fallback int) int {
|
|
|
|
|
if v := int(target.Load()); v > 0 {
|
2026-03-13 09:28:16 -05:00
|
|
|
return v
|
|
|
|
|
}
|
2026-07-21 16:08:45 -05:00
|
|
|
return fallback
|
2026-03-13 09:28:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 14:45:15 -06:00
|
|
|
func ensureH264EncoderLocked(width, height int) error {
|
2026-03-13 09:28:16 -05:00
|
|
|
fps := activeH264FPS()
|
|
|
|
|
if h264Enc != nil && h264Width == width && h264Height == height && h264FPS == fps {
|
2026-03-05 14:45:15 -06:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeH264EncoderLocked()
|
|
|
|
|
|
|
|
|
|
opts := &x264.Options{
|
2026-03-06 18:47:12 -06:00
|
|
|
Width: width,
|
|
|
|
|
Height: height,
|
2026-03-13 09:28:16 -05:00
|
|
|
FrameRate: fps,
|
|
|
|
|
Tune: "zerolatency",
|
|
|
|
|
Preset: "veryfast",
|
|
|
|
|
Profile: "main",
|
2026-03-06 18:47:12 -06:00
|
|
|
RateControl: "crf",
|
2026-07-17 04:28:23 -05:00
|
|
|
RateConstant: targetH264CRF(width, height, fps),
|
2026-03-06 18:47:12 -06:00
|
|
|
LogLevel: x264.LogError,
|
2026-03-05 14:45:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enc, err := x264.NewEncoder(&h264Buf, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h264Enc = enc
|
|
|
|
|
h264Width = width
|
|
|
|
|
h264Height = height
|
2026-03-13 09:28:16 -05:00
|
|
|
h264FPS = fps
|
2026-03-05 14:45:15 -06:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resetH264Encoder() {
|
|
|
|
|
h264Mu.Lock()
|
|
|
|
|
defer h264Mu.Unlock()
|
|
|
|
|
closeH264EncoderLocked()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 17:18:46 -05:00
|
|
|
func RequestDesktopH264Keyframe() {
|
|
|
|
|
resetH264Encoder()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 14:45:15 -06:00
|
|
|
func closeH264EncoderLocked() {
|
|
|
|
|
if h264Enc != nil {
|
|
|
|
|
_ = h264Enc.Close()
|
|
|
|
|
h264Enc = nil
|
|
|
|
|
}
|
|
|
|
|
h264Width = 0
|
|
|
|
|
h264Height = 0
|
2026-03-13 09:28:16 -05:00
|
|
|
h264FPS = 0
|
2026-03-05 14:45:15 -06:00
|
|
|
h264Buf.Reset()
|
|
|
|
|
}
|
2026-05-21 21:11:10 -05:00
|
|
|
|
|
|
|
|
var (
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Mu sync.Mutex
|
|
|
|
|
backstageH264Enc *x264.Encoder
|
|
|
|
|
backstageH264Buf bytes.Buffer
|
|
|
|
|
backstageH264Width int
|
|
|
|
|
backstageH264Height int
|
|
|
|
|
backstageH264FPS int
|
|
|
|
|
backstageH264Scratch []byte
|
2026-05-21 21:11:10 -05:00
|
|
|
)
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
func encodeH264Framebackstage(img *image.RGBA) ([]byte, error) {
|
2026-05-21 21:11:10 -05:00
|
|
|
bounds := img.Bounds()
|
|
|
|
|
width := bounds.Dx()
|
|
|
|
|
height := bounds.Dy()
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Mu.Lock()
|
|
|
|
|
defer backstageH264Mu.Unlock()
|
2026-05-21 21:11:10 -05:00
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
if err := ensurebackstageH264EncoderLocked(width, height); err != nil {
|
2026-05-21 21:11:10 -05:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Buf.Reset()
|
|
|
|
|
if err := backstageH264Enc.Encode(img); err != nil {
|
2026-05-21 21:11:10 -05:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
n := backstageH264Buf.Len()
|
2026-05-21 21:11:10 -05:00
|
|
|
if n == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
if cap(backstageH264Scratch) < n {
|
|
|
|
|
backstageH264Scratch = make([]byte, n, n+(n/4))
|
2026-05-21 21:11:10 -05:00
|
|
|
} else {
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Scratch = backstageH264Scratch[:n]
|
2026-05-21 21:11:10 -05:00
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
copy(backstageH264Scratch, backstageH264Buf.Bytes())
|
|
|
|
|
return backstageH264Scratch, nil
|
2026-05-21 21:11:10 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
func resetH264Encoderbackstage() {
|
|
|
|
|
backstageH264Mu.Lock()
|
|
|
|
|
defer backstageH264Mu.Unlock()
|
|
|
|
|
closebackstageH264EncoderLocked()
|
2026-05-21 21:11:10 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-21 16:08:45 -05:00
|
|
|
func RequestBackstageH264Keyframe() {
|
|
|
|
|
resetH264Encoderbackstage()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
func ensurebackstageH264EncoderLocked(width, height int) error {
|
2026-07-21 16:08:45 -05:00
|
|
|
fps := activeH264FPSValue(&backstageH264TargetFPS, 60)
|
2026-07-13 10:22:19 -05:00
|
|
|
if backstageH264Enc != nil && backstageH264Width == width && backstageH264Height == height && backstageH264FPS == fps {
|
2026-05-21 21:11:10 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
closebackstageH264EncoderLocked()
|
2026-05-21 21:11:10 -05:00
|
|
|
opts := &x264.Options{
|
|
|
|
|
Width: width,
|
|
|
|
|
Height: height,
|
|
|
|
|
FrameRate: fps,
|
|
|
|
|
Tune: "zerolatency",
|
|
|
|
|
Preset: "veryfast",
|
|
|
|
|
Profile: "main",
|
|
|
|
|
RateControl: "crf",
|
2026-07-17 04:28:23 -05:00
|
|
|
RateConstant: targetH264CRF(width, height, fps),
|
2026-05-21 21:11:10 -05:00
|
|
|
LogLevel: x264.LogError,
|
|
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
enc, err := x264.NewEncoder(&backstageH264Buf, opts)
|
2026-05-21 21:11:10 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Enc = enc
|
|
|
|
|
backstageH264Width = width
|
|
|
|
|
backstageH264Height = height
|
|
|
|
|
backstageH264FPS = fps
|
2026-05-21 21:11:10 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 10:22:19 -05:00
|
|
|
func closebackstageH264EncoderLocked() {
|
|
|
|
|
if backstageH264Enc != nil {
|
|
|
|
|
_ = backstageH264Enc.Close()
|
|
|
|
|
backstageH264Enc = nil
|
2026-05-21 21:11:10 -05:00
|
|
|
}
|
2026-07-13 10:22:19 -05:00
|
|
|
backstageH264Width = 0
|
|
|
|
|
backstageH264Height = 0
|
|
|
|
|
backstageH264FPS = 0
|
|
|
|
|
backstageH264Buf.Reset()
|
2026-05-21 21:11:10 -05:00
|
|
|
}
|
2026-05-21 21:26:55 -05:00
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
webcamH264Mu sync.Mutex
|
|
|
|
|
webcamH264Enc *x264.Encoder
|
|
|
|
|
webcamH264Buf bytes.Buffer
|
|
|
|
|
webcamH264Width int
|
|
|
|
|
webcamH264Height int
|
|
|
|
|
webcamH264FPS int
|
|
|
|
|
webcamH264Scratch []byte
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func encodeH264FrameWebcam(img *image.RGBA) ([]byte, error) {
|
|
|
|
|
bounds := img.Bounds()
|
|
|
|
|
width := bounds.Dx()
|
|
|
|
|
height := bounds.Dy()
|
|
|
|
|
|
|
|
|
|
webcamH264Mu.Lock()
|
|
|
|
|
defer webcamH264Mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if err := ensureWebcamH264EncoderLocked(width, height); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
webcamH264Buf.Reset()
|
|
|
|
|
if err := webcamH264Enc.Encode(img); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
n := webcamH264Buf.Len()
|
|
|
|
|
if n == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if cap(webcamH264Scratch) < n {
|
|
|
|
|
webcamH264Scratch = make([]byte, n, n+(n/4))
|
|
|
|
|
} else {
|
|
|
|
|
webcamH264Scratch = webcamH264Scratch[:n]
|
|
|
|
|
}
|
|
|
|
|
copy(webcamH264Scratch, webcamH264Buf.Bytes())
|
|
|
|
|
return webcamH264Scratch, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ensureWebcamH264EncoderLocked(width, height int) error {
|
2026-07-21 16:08:45 -05:00
|
|
|
fps := activeH264FPSValue(&webcamH264TargetFPS, 30)
|
2026-05-21 21:26:55 -05:00
|
|
|
if webcamH264Enc != nil && webcamH264Width == width && webcamH264Height == height && webcamH264FPS == fps {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
closeWebcamH264EncoderLocked()
|
|
|
|
|
opts := &x264.Options{
|
|
|
|
|
Width: width,
|
|
|
|
|
Height: height,
|
|
|
|
|
FrameRate: fps,
|
|
|
|
|
Tune: "zerolatency",
|
|
|
|
|
Preset: "veryfast",
|
|
|
|
|
Profile: "main",
|
|
|
|
|
RateControl: "crf",
|
2026-07-17 04:28:23 -05:00
|
|
|
RateConstant: targetH264CRF(width, height, fps),
|
2026-05-21 21:26:55 -05:00
|
|
|
LogLevel: x264.LogError,
|
|
|
|
|
}
|
|
|
|
|
enc, err := x264.NewEncoder(&webcamH264Buf, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
webcamH264Enc = enc
|
|
|
|
|
webcamH264Width = width
|
|
|
|
|
webcamH264Height = height
|
|
|
|
|
webcamH264FPS = fps
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func closeWebcamH264EncoderLocked() {
|
|
|
|
|
if webcamH264Enc != nil {
|
|
|
|
|
_ = webcamH264Enc.Close()
|
|
|
|
|
webcamH264Enc = nil
|
|
|
|
|
}
|
|
|
|
|
webcamH264Width = 0
|
|
|
|
|
webcamH264Height = 0
|
|
|
|
|
webcamH264FPS = 0
|
|
|
|
|
webcamH264Buf.Reset()
|
|
|
|
|
}
|