feat: add PipeWire screen and system audio capture (M2)
Capture the Wayland desktop via xdg-desktop-portal + PipeWire using go2tv.app/screencast (MIT), and stream it to OBS: - internal/capture: Capture/FrameSource/AudioSource interfaces and the PipeWire backend (BGRA frames at monitor resolution, S16 48 kHz stereo system audio) - protocol: EncodeBGRA fast path producing 4:2:0 YCbCr JPEGs - cmd: --source screen|pattern, --audio, --stream-index flags; real capture feeds the existing sender - share one wall-clock reference between the audio and video loops so OBS receives aligned A/V timestamps (avoids multi-second latency) Verified end-to-end: real desktop at 30 fps renders in OBS with sub-second latency.
This commit is contained in:
@@ -74,6 +74,47 @@ func (e *JPEGEncoder) Encode(img image.Image, quality int) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeBGRA compresses a raw BGRA (blue, green, red, alpha) pixel buffer of
|
||||
// the given dimensions. This is the fast path for the PipeWire screen-capture
|
||||
// backend, which delivers frames in BGRA byte order. Subsampling defaults to
|
||||
// 4:2:0 (a YCbCr JPEG), so the OBS receiver decodes it as I420 — the same
|
||||
// layout obs-teleport produces for OBS-native YCbCr frames.
|
||||
func (e *JPEGEncoder) EncodeBGRA(pix []byte, width, height, quality int) ([]byte, error) {
|
||||
if quality < 1 {
|
||||
quality = 1
|
||||
}
|
||||
if quality > 100 {
|
||||
quality = 100
|
||||
}
|
||||
|
||||
want := width * height * 4
|
||||
if len(pix) < want {
|
||||
return nil, errors.New("turbojpeg: BGRA buffer too small")
|
||||
}
|
||||
|
||||
C.tj3Set(e.ctx, C.TJPARAM_QUALITY, C.int(quality))
|
||||
C.tj3Set(e.ctx, C.TJPARAM_SUBSAMP, C.TJSAMP_420)
|
||||
C.tj3Set(e.ctx, C.TJPARAM_COLORSPACE, C.TJCS_YCbCr)
|
||||
|
||||
size := C.tj3JPEGBufSize(C.int(width), C.int(height), C.TJSAMP_420)
|
||||
buf := make([]byte, int(size))
|
||||
|
||||
srcPtr := unsafe.Pointer(&pix[0])
|
||||
dstPtr := (*C.uchar)(&buf[0])
|
||||
|
||||
var pin runtime.Pinner
|
||||
pin.Pin(srcPtr)
|
||||
pin.Pin(dstPtr)
|
||||
defer pin.Unpin()
|
||||
|
||||
jpegSize := size
|
||||
rc := C.tj3Compress8(e.ctx, (*C.uchar)(srcPtr), C.int(width), 0, C.int(height), C.TJPF_BGRA, &dstPtr, &jpegSize)
|
||||
if rc != 0 {
|
||||
return nil, errors.New("turbojpeg BGRA compress failed")
|
||||
}
|
||||
return buf[:int(jpegSize)], nil
|
||||
}
|
||||
|
||||
// encodeRGBA compresses a Go RGBA image (pixel layout [R,G,B,A] per 4 bytes).
|
||||
// TJPF_RGBA tells turbojpeg the exact layout; colourspace is RGB.
|
||||
func (e *JPEGEncoder) encodeRGBA(img *image.RGBA) ([]byte, error) {
|
||||
|
||||
Reference in New Issue
Block a user