Add the support of screenshotting using libav
Some checks failed
rolling-release / build (push) Has been cancelled
rolling-release / rolling-release (push) Has been cancelled

This commit is contained in:
Dmitrii Okunev
2025-08-09 01:42:14 +01:00
parent 30db31ff36
commit c79ee62fa8
8 changed files with 165 additions and 21 deletions

2
go.mod
View File

@@ -298,7 +298,7 @@ require (
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/xaionaro-go/audio v0.0.0-20250210102901-abfced9d5ef3
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2
github.com/xaionaro-go/datacounter v1.0.4
github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42
github.com/xaionaro-go/grpcproxy v0.0.0-20241103205849-a8fef42e72f9

4
go.sum
View File

@@ -1078,6 +1078,10 @@ github.com/xaionaro-go/avmediacodec v0.0.0-20250505012527-c819676502d8 h1:FZn9+T
github.com/xaionaro-go/avmediacodec v0.0.0-20250505012527-c819676502d8/go.mod h1:2W2Kp/HJFXcFBppQ4YytgDy/ydFL3hGc23xSB1U/Luc=
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18 h1:OfkJnBBNbr3AUbqumbGpY78W8FoEHaz0zKdKzZMmkGc=
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
github.com/xaionaro-go/avpipeline v0.0.0-20250808224324-1125ac2d144b h1:J+fc4WXTCnxc1LfafpRndcWOjq0NDCpToc8NPBHdSIQ=
github.com/xaionaro-go/avpipeline v0.0.0-20250808224324-1125ac2d144b/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2 h1:xKetGyk2/9XGZwiupCYGJLoAkw2dl774Gp4afxLbZoY=
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
github.com/xaionaro-go/datacounter v1.0.4 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
github.com/xaionaro-go/eventbus v0.0.0-20250720144534-4670758005d9 h1:ZAm8ueMw5D85LDeV1Kboc3ANqXr3LK/eXIl9hj1BJyM=

View File

@@ -0,0 +1,7 @@
package main
func assertNoError(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,50 @@
package main
import (
"context"
"flag"
"fmt"
"image"
"time"
"github.com/xaionaro-go/streamctl/pkg/screenshot"
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
)
func main() {
xMin := flag.Int("x-min", 0, "")
xMax := flag.Int("x-max", 100, "")
yMin := flag.Int("y-min", 0, "")
yMax := flag.Int("y-max", 100, "")
fps := flag.Float64("fps", 30, "")
flag.Parse()
ctx := context.Background()
h := screenshoter.New()
startedAt := time.Now()
frameCount := 0
err := h.Loop(
ctx,
time.Duration(float64(time.Second) / *fps),
screenshot.Config{
Bounds: image.Rectangle{
Min: image.Point{
X: *xMin,
Y: *yMin,
},
Max: image.Point{
X: *xMax,
Y: *yMax,
},
},
},
func(context.Context, image.Image) {
frameCount++
fps := float64(frameCount) / time.Since(startedAt).Seconds()
fmt.Printf("received a picture; overall FPS: %f\n", fps)
},
)
assertNoError(err)
}

View File

@@ -0,0 +1,80 @@
//go:build with_libav && linux
// +build with_libav,linux
package screenshoter
import (
"context"
"fmt"
"image"
"time"
"github.com/asticode/go-astiav"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/avpipeline/frame"
"github.com/xaionaro-go/avpipeline/node"
"github.com/xaionaro-go/avpipeline/preset/screencapturer"
"github.com/xaionaro-go/observability"
"github.com/xaionaro-go/streamctl/pkg/screenshot"
)
type Screenshoter struct{}
func New() *Screenshoter {
return &Screenshoter{}
}
func (s *Screenshoter) Loop(
ctx context.Context,
interval time.Duration,
config screenshot.Config,
callback func(context.Context, image.Image),
) error {
fpsFloat := 1 / interval.Seconds()
fps := astiav.Rational{}
fps.SetNum(int(fpsFloat * 1000))
fps.SetDen(1000)
logger.Debugf(ctx, "FPS == %f", fps.Float64())
screenCapturer, err := screencapturer.New(ctx, screencapturer.Params{
Area: config.Bounds,
FPS: fps,
})
if err != nil {
return fmt.Errorf("unable to initialize a screen capturer: %w", err)
}
errCh := make(chan node.Error, 100)
observability.Go(ctx, func(ctx context.Context) {
screenCapturer.Serve(ctx, node.ServeConfig{}, errCh)
})
defer screenCapturer.Close(ctx)
var img image.Image
for {
var frame frame.Output
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errCh:
return fmt.Errorf("unable to serve the screenshoter node: %w", err)
case pkt := <-screenCapturer.OutputPacketChan():
return fmt.Errorf("receive a packet (of size %d), while expected a decoded frame", pkt.GetSize())
case frame = <-screenCapturer.OutputFrameChan():
}
if img == nil {
img, err = frame.Data().GuessImageFormat()
if err != nil {
return fmt.Errorf("unable to guess the image format: %w", err)
}
}
err := frame.Data().ToImage(img)
if err != nil {
return fmt.Errorf("unable to obtain the image: %w", err)
}
callback(ctx, img)
}
}

View File

@@ -1,3 +1,6 @@
//go:build !with_libav || !linux
// +build !with_libav !linux
package screenshoter
import (
@@ -10,37 +13,37 @@ import (
)
type ScreenshotEngine interface {
Screenshot(cfg screenshot.Config) (*image.RGBA, error)
Screenshot(cfg screenshot.Config) (image.Image, error)
}
type Screenshoter struct {
ScreenshotEngine ScreenshotEngine
}
func New(
engine ScreenshotEngine,
) *Screenshoter {
return &Screenshoter{
ScreenshotEngine: engine,
}
type ScreenshotImplementation struct{}
func (ScreenshotImplementation) Screenshot(cfg screenshot.Config) (image.Image, error) {
return screenshot.Implementation{}.Screenshot(cfg)
}
func (s *Screenshoter) Engine() ScreenshotEngine {
return s.ScreenshotEngine
func New() *Screenshoter {
return &Screenshoter{
ScreenshotEngine: ScreenshotImplementation{},
}
}
func (s *Screenshoter) Loop(
ctx context.Context,
interval time.Duration,
config screenshot.Config,
callback func(context.Context, *image.RGBA),
) {
callback func(context.Context, image.Image),
) error {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
return ctx.Err()
case <-t.C:
}
img, err := s.ScreenshotEngine.Screenshot(config)

View File

@@ -17,20 +17,18 @@ import (
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/observability"
"github.com/xaionaro-go/streamctl/pkg/screenshot"
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
"github.com/xaionaro-go/xsync"
)
type Screenshoter interface {
Engine() screenshoter.ScreenshotEngine
Loop(
ctx context.Context,
interval time.Duration,
config screenshot.Config,
callback func(context.Context, *image.RGBA),
)
callback func(context.Context, image.Image),
) error
}
func (p *Panel) setImage(
@@ -313,12 +311,15 @@ func (p *Panel) reinitScreenshoter(ctx context.Context) {
ctx, cancelFunc := context.WithCancel(ctx)
p.screenshoterClose = cancelFunc
observability.Go(ctx, func(ctx context.Context) {
p.Screenshoter.Loop(
err := p.Screenshoter.Loop(
ctx,
200*time.Millisecond,
p.Config.Screenshot.Config,
func(ctx context.Context, img *image.RGBA) { p.setScreenshot(ctx, img) },
func(ctx context.Context, img image.Image) { p.setScreenshot(ctx, img) },
)
if err != nil {
logger.Errorf(ctx, "unable to run the screenshoter loop: %v", err)
}
})
})
}

View File

@@ -34,7 +34,6 @@ import (
"github.com/xaionaro-go/streamctl/pkg/command"
gconsts "github.com/xaionaro-go/streamctl/pkg/consts"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
"github.com/xaionaro-go/streamctl/pkg/screenshot"
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
@@ -204,7 +203,7 @@ func New(
p := &Panel{
configPath: configPath,
Config: Options(opts).ApplyOverrides(cfg),
Screenshoter: screenshoter.New(screenshot.Implementation{}),
Screenshoter: screenshoter.New(),
imageLastDownloaded: map[consts.ImageID][]byte{},
imageLastParsed: map[consts.ImageID]image.Image{},
streamStatus: map[streamcontrol.PlatformName]*streamStatus{},