feat: screen capture scaling / downsampling
Add a Scale option (1.0 native, 0.5 half, etc.) that downsamples the captured BGRA frame with bilinear interpolation before JPEG encoding. Config carries the scale factor; the GUI exposes a Scale dropdown (100%/75%/50%/25%) that applies live via SetConfig. Verified: OBS displays the image at the reduced resolution matching the selected scale, without restarting the stream.
This commit is contained in:
@@ -50,6 +50,9 @@ type Config struct {
|
||||
Audio bool
|
||||
// StreamIndex selects which monitor to capture (screen source only).
|
||||
StreamIndex int
|
||||
// Scale downscales the captured frame before encoding. 1.0 = native
|
||||
// resolution, 0.5 = half width/height, etc. Must be in (0, 1].
|
||||
Scale float64
|
||||
// Announce controls whether the stream is advertised via UDP multicast.
|
||||
// When disabled, receivers must connect by IP manually.
|
||||
Announce bool
|
||||
@@ -63,6 +66,7 @@ func DefaultConfig() Config {
|
||||
FPS: 30,
|
||||
Source: "screen",
|
||||
Audio: true,
|
||||
Scale: 1.0,
|
||||
Announce: true,
|
||||
}
|
||||
}
|
||||
@@ -84,6 +88,9 @@ func (c Config) Validate() error {
|
||||
if c.StreamIndex < 0 {
|
||||
return errors.New("stream index must be >= 0")
|
||||
}
|
||||
if c.Scale <= 0 || c.Scale > 1 {
|
||||
return errors.New("scale must be in (0, 1]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -117,6 +124,10 @@ type Engine struct {
|
||||
|
||||
frames atomic.Int64
|
||||
|
||||
// scaleBuf is the cached downscaled frame, reallocated only when the
|
||||
// configured scale or source dimensions change.
|
||||
scaleBuf *capture.VideoFrame
|
||||
|
||||
errMu sync.RWMutex
|
||||
lastErr error
|
||||
|
||||
@@ -348,6 +359,72 @@ func (e *Engine) audioLoop(src io.ReadCloser) {
|
||||
}
|
||||
}
|
||||
|
||||
// scaleFrame downscales frame by the configured factor, returning the original
|
||||
// when scale is 1.0 (native). The result is a cached buffer reused across
|
||||
// frames, reallocated only when the dimensions or scale change.
|
||||
func (e *Engine) scaleFrame(frame *capture.VideoFrame, scale float64) *capture.VideoFrame {
|
||||
if scale >= 1.0 || frame == nil {
|
||||
return frame
|
||||
}
|
||||
|
||||
sw := int(float64(frame.Width) * scale)
|
||||
sh := int(float64(frame.Height) * scale)
|
||||
if sw < 1 {
|
||||
sw = 1
|
||||
}
|
||||
if sh < 1 {
|
||||
sh = 1
|
||||
}
|
||||
|
||||
// Reuse the cached buffer if it matches the target size.
|
||||
if e.scaleBuf == nil || e.scaleBuf.Width != sw || e.scaleBuf.Height != sh {
|
||||
e.scaleBuf = &capture.VideoFrame{
|
||||
Pix: make([]byte, sw*sh*4),
|
||||
Width: sw,
|
||||
Height: sh,
|
||||
Stride: sw * 4,
|
||||
}
|
||||
}
|
||||
|
||||
scaleBGRA(frame, e.scaleBuf)
|
||||
return e.scaleBuf
|
||||
}
|
||||
|
||||
// scaleBGRA bilinearly downsamples an interleaved BGRA frame into dst.
|
||||
func scaleBGRA(src, dst *capture.VideoFrame) {
|
||||
sw, sh := float64(src.Width), float64(src.Height)
|
||||
for y := 0; y < dst.Height; y++ {
|
||||
srcY := (float64(y) + 0.5) * sh / float64(dst.Height)
|
||||
y0 := int(srcY)
|
||||
if y0 >= src.Height-1 {
|
||||
y0 = src.Height - 2
|
||||
}
|
||||
yFrac := srcY - float64(y0)
|
||||
row0 := y0 * src.Stride
|
||||
row1 := (y0 + 1) * src.Stride
|
||||
di := y * dst.Stride
|
||||
|
||||
for x := 0; x < dst.Width; x++ {
|
||||
srcX := (float64(x) + 0.5) * sw / float64(dst.Width)
|
||||
x0 := int(srcX)
|
||||
if x0 >= src.Width-1 {
|
||||
x0 = src.Width - 2
|
||||
}
|
||||
xFrac := srcX - float64(x0)
|
||||
p00 := row0 + x0*4
|
||||
p01 := row0 + (x0+1)*4
|
||||
p10 := row1 + x0*4
|
||||
p11 := row1 + (x0+1)*4
|
||||
|
||||
for c := 0; c < 4; c++ {
|
||||
top := float64(src.Pix[p00+c])*(1-xFrac) + float64(src.Pix[p01+c])*xFrac
|
||||
bot := float64(src.Pix[p10+c])*(1-xFrac) + float64(src.Pix[p11+c])*xFrac
|
||||
dst.Pix[di+x*4+c] = uint8(top*(1-yFrac) + bot*yFrac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// videoLoop pulls frames and sends them at the configured fps. The frame rate
|
||||
// and JPEG quality are read from the live config so SetConfig takes effect
|
||||
// without restarting.
|
||||
@@ -388,7 +465,8 @@ func (e *Engine) videoLoop() {
|
||||
next = now.Add(frameInterval)
|
||||
|
||||
ts := uint64(now.Sub(e.start))
|
||||
buf, err := e.encoder.EncodeBGRA(frame.Pix, frame.Width, frame.Height, cfg.Quality)
|
||||
enc := e.scaleFrame(frame, cfg.Scale)
|
||||
buf, err := e.encoder.EncodeBGRA(enc.Pix, enc.Width, enc.Height, cfg.Quality)
|
||||
if err != nil {
|
||||
e.setErr(err)
|
||||
log.Printf("flinger: jpeg: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user