diff --git a/go.mod b/go.mod index cc28cca..82963c7 100755 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 65d0ea8..100f0e8 100755 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/screenshoter/cmd/screenshoter/assert.go b/pkg/screenshoter/cmd/screenshoter/assert.go new file mode 100644 index 0000000..18fcaa8 --- /dev/null +++ b/pkg/screenshoter/cmd/screenshoter/assert.go @@ -0,0 +1,7 @@ +package main + +func assertNoError(err error) { + if err != nil { + panic(err) + } +} diff --git a/pkg/screenshoter/cmd/screenshoter/main.go b/pkg/screenshoter/cmd/screenshoter/main.go new file mode 100644 index 0000000..f44bfa2 --- /dev/null +++ b/pkg/screenshoter/cmd/screenshoter/main.go @@ -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) +} diff --git a/pkg/screenshoter/screenshoter_libav.go b/pkg/screenshoter/screenshoter_libav.go new file mode 100644 index 0000000..b0920fb --- /dev/null +++ b/pkg/screenshoter/screenshoter_libav.go @@ -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) + } +} diff --git a/pkg/screenshoter/screenshoter.go b/pkg/screenshoter/screenshoter_nolibav.go similarity index 60% rename from pkg/screenshoter/screenshoter.go rename to pkg/screenshoter/screenshoter_nolibav.go index b019c7d..6f37e6a 100644 --- a/pkg/screenshoter/screenshoter.go +++ b/pkg/screenshoter/screenshoter_nolibav.go @@ -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) diff --git a/pkg/streampanel/image.go b/pkg/streampanel/image.go index f5ea991..296aee4 100644 --- a/pkg/streampanel/image.go +++ b/pkg/streampanel/image.go @@ -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) + } }) }) } diff --git a/pkg/streampanel/panel.go b/pkg/streampanel/panel.go index ce48bfb..5b6d8db 100644 --- a/pkg/streampanel/panel.go +++ b/pkg/streampanel/panel.go @@ -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{},