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/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/xaionaro-go/audio v0.0.0-20250210102901-abfced9d5ef3 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/datacounter v1.0.4
github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42 github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42
github.com/xaionaro-go/grpcproxy v0.0.0-20241103205849-a8fef42e72f9 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/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 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-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 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c= 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= 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 package screenshoter
import ( import (
@@ -10,37 +13,37 @@ import (
) )
type ScreenshotEngine interface { type ScreenshotEngine interface {
Screenshot(cfg screenshot.Config) (*image.RGBA, error) Screenshot(cfg screenshot.Config) (image.Image, error)
} }
type Screenshoter struct { type Screenshoter struct {
ScreenshotEngine ScreenshotEngine ScreenshotEngine ScreenshotEngine
} }
func New( type ScreenshotImplementation struct{}
engine ScreenshotEngine,
) *Screenshoter { func (ScreenshotImplementation) Screenshot(cfg screenshot.Config) (image.Image, error) {
return &Screenshoter{ return screenshot.Implementation{}.Screenshot(cfg)
ScreenshotEngine: engine,
}
} }
func (s *Screenshoter) Engine() ScreenshotEngine { func New() *Screenshoter {
return s.ScreenshotEngine return &Screenshoter{
ScreenshotEngine: ScreenshotImplementation{},
}
} }
func (s *Screenshoter) Loop( func (s *Screenshoter) Loop(
ctx context.Context, ctx context.Context,
interval time.Duration, interval time.Duration,
config screenshot.Config, config screenshot.Config,
callback func(context.Context, *image.RGBA), callback func(context.Context, image.Image),
) { ) error {
t := time.NewTicker(interval) t := time.NewTicker(interval)
defer t.Stop() defer t.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return ctx.Err()
case <-t.C: case <-t.C:
} }
img, err := s.ScreenshotEngine.Screenshot(config) img, err := s.ScreenshotEngine.Screenshot(config)

View File

@@ -17,20 +17,18 @@ import (
"github.com/facebookincubator/go-belt/tool/logger" "github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/observability" "github.com/xaionaro-go/observability"
"github.com/xaionaro-go/streamctl/pkg/screenshot" "github.com/xaionaro-go/streamctl/pkg/screenshot"
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts" streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts" "github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
"github.com/xaionaro-go/xsync" "github.com/xaionaro-go/xsync"
) )
type Screenshoter interface { type Screenshoter interface {
Engine() screenshoter.ScreenshotEngine
Loop( Loop(
ctx context.Context, ctx context.Context,
interval time.Duration, interval time.Duration,
config screenshot.Config, config screenshot.Config,
callback func(context.Context, *image.RGBA), callback func(context.Context, image.Image),
) ) error
} }
func (p *Panel) setImage( func (p *Panel) setImage(
@@ -313,12 +311,15 @@ func (p *Panel) reinitScreenshoter(ctx context.Context) {
ctx, cancelFunc := context.WithCancel(ctx) ctx, cancelFunc := context.WithCancel(ctx)
p.screenshoterClose = cancelFunc p.screenshoterClose = cancelFunc
observability.Go(ctx, func(ctx context.Context) { observability.Go(ctx, func(ctx context.Context) {
p.Screenshoter.Loop( err := p.Screenshoter.Loop(
ctx, ctx,
200*time.Millisecond, 200*time.Millisecond,
p.Config.Screenshot.Config, 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" "github.com/xaionaro-go/streamctl/pkg/command"
gconsts "github.com/xaionaro-go/streamctl/pkg/consts" gconsts "github.com/xaionaro-go/streamctl/pkg/consts"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler" "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/screenshoter"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol" "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick" "github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
@@ -204,7 +203,7 @@ func New(
p := &Panel{ p := &Panel{
configPath: configPath, configPath: configPath,
Config: Options(opts).ApplyOverrides(cfg), Config: Options(opts).ApplyOverrides(cfg),
Screenshoter: screenshoter.New(screenshot.Implementation{}), Screenshoter: screenshoter.New(),
imageLastDownloaded: map[consts.ImageID][]byte{}, imageLastDownloaded: map[consts.ImageID][]byte{},
imageLastParsed: map[consts.ImageID]image.Image{}, imageLastParsed: map[consts.ImageID]image.Image{},
streamStatus: map[streamcontrol.PlatformName]*streamStatus{}, streamStatus: map[streamcontrol.PlatformName]*streamStatus{},