Start refactoring the screenshoter

This commit is contained in:
Dmitrii Okunev
2025-08-09 01:42:14 +01:00
parent 30db31ff36
commit 9565cfa77b
7 changed files with 135 additions and 31 deletions

6
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-20250809014515-3d56d4095dc4
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
@@ -310,8 +310,8 @@ require (
github.com/xaionaro-go/object v0.0.0-20241026212449-753ce10ec94c
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a
github.com/xaionaro-go/observability v0.0.0-20250622130956-24b7017284e4
github.com/xaionaro-go/player v0.0.0-20250805195658-8ea6ef10e5ff
github.com/xaionaro-go/recoder v0.0.0-20250805195121-617f4a6a1506
github.com/xaionaro-go/player v0.0.0-20250809010311-a80dbec6c520
github.com/xaionaro-go/recoder v0.0.0-20250809010016-1241e0b14473
github.com/xaionaro-go/secret v0.0.0-20250111141743-ced12e1082c2
github.com/xaionaro-go/serializable v0.0.0-20250412140540-5ac572306599
github.com/xaionaro-go/timeapiio v0.0.0-20240915203246-b907cf699af3

10
go.sum
View File

@@ -1076,8 +1076,8 @@ github.com/xaionaro-go/avcommon v0.0.0-20250805213258-459b927db31a h1:dAFZ5UCjmT
github.com/xaionaro-go/avcommon v0.0.0-20250805213258-459b927db31a/go.mod h1:kjLo1LasgdDJqbTGD5bbEM+D6RiZSbf5ZT8yiPFF1BA=
github.com/xaionaro-go/avmediacodec v0.0.0-20250505012527-c819676502d8 h1:FZn9+TN3uHhohfpanWkR9lFNHApizznZbML6XjvEgTU=
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-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=
@@ -1124,12 +1124,14 @@ github.com/xaionaro-go/observability v0.0.0-20250622130956-24b7017284e4 h1:dlsJ1
github.com/xaionaro-go/observability v0.0.0-20250622130956-24b7017284e4/go.mod h1:GkNC0+nPJMOzotTzJVlH4+DvDg9cKrV9qqOe+8/kVw4=
github.com/xaionaro-go/player v0.0.0-20250805195658-8ea6ef10e5ff h1:sYAwCUWGRd949aybdufcTpRsZ9go4sZ8ZTpLRF3KZe0=
github.com/xaionaro-go/player v0.0.0-20250805195658-8ea6ef10e5ff/go.mod h1:ZPfygPU8g18dyvOnYCPdjvHT7h1Q80eG/rfmWOT6Vls=
github.com/xaionaro-go/player v0.0.0-20250809010311-a80dbec6c520 h1:1KtKt2EBle1k7Tk7Kfg5vT92RHC6u7C3FtzLVKe1cAM=
github.com/xaionaro-go/player v0.0.0-20250809010311-a80dbec6c520/go.mod h1:9XTkb7WxVOYnW+UJ1+ajMzueOW1nv4uuRsPADWmzH0M=
github.com/xaionaro-go/proxy v0.0.0-20250525144747-579f5a891c15 h1:Qqoy9MDWq2Yh6uazAqQDzqU0doalTL3tRjNCo7X7GXA=
github.com/xaionaro-go/proxy v0.0.0-20250525144747-579f5a891c15/go.mod h1:6kxHtLmOImv/zwXSvaI1CW9Q8Pw+m5b891ZoejMKHPA=
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb h1:9iHPI27CYbmJDhzEuCABQthE/DGVNvT60ybWvv3BV8w=
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/xaionaro-go/recoder v0.0.0-20250805195121-617f4a6a1506 h1:0kFR1IYlNdr8WdomIXe9zndAHdqI+9Z+yxyzT33sw58=
github.com/xaionaro-go/recoder v0.0.0-20250805195121-617f4a6a1506/go.mod h1:LStqQ27RH8oRj0rMC3zSzzfHms9vuRAorrZU8gP9WYg=
github.com/xaionaro-go/recoder v0.0.0-20250809010016-1241e0b14473 h1:VYIpYsjoZ3NdvaGLyMKR0yRqzb3vxyXI8/5LW8nu97w=
github.com/xaionaro-go/recoder v0.0.0-20250809010016-1241e0b14473/go.mod h1:Gv9ermjaTFxgqb499Jt6aHrWP/hNgRsqSvlQixG3sa4=
github.com/xaionaro-go/secret v0.0.0-20250111141743-ced12e1082c2 h1:QHpTWfyfmz65cE0MtFXe9fScdi+X0VIYR2wgolSYEUk=
github.com/xaionaro-go/secret v0.0.0-20250111141743-ced12e1082c2/go.mod h1:XKoHGZ4VKMbVBl8VotLIoWQdrB6Q7jnR++RbkiegZFU=
github.com/xaionaro-go/serializable v0.0.0-20250412140540-5ac572306599 h1:CzcQd6wLiqgjd8K/6UzR5uyt6sg4ut/kVxi6+FJMbdI=

View File

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

View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"flag"
"fmt"
"image"
"image/png"
"os"
"time"
"github.com/facebookincubator/go-belt"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/facebookincubator/go-belt/tool/logger/implementation/logrus"
"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, "")
outputFile := flag.String("output-file", "", "")
flag.Parse()
l := logrus.Default().WithLevel(logger.LevelDebug)
ctx := context.Background()
ctx = logger.CtxWithLogger(ctx, l)
logger.Default = func() logger.Logger {
return l
}
defer belt.Flush(ctx)
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, img image.Image) {
frameCount++
fps := float64(frameCount) / time.Since(startedAt).Seconds()
fmt.Printf("received a picture; overall FPS: %f\n", fps)
if *outputFile == "" {
return
}
func() {
f, err := os.OpenFile(*outputFile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logger.Error(ctx, "unable to open '%s'", *outputFile)
return
}
defer f.Close()
err = png.Encode(f, img)
if err != nil {
logger.Errorf(ctx, "unable to encode the image to PNG: %w", err)
return
}
}()
},
)
assertNoError(err)
}

View File

@@ -10,37 +10,38 @@ 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 {
type ScreenshotImplementation struct{}
func (ScreenshotImplementation) Screenshot(cfg screenshot.Config) (image.Image, error) {
return screenshot.Implementation{}.Screenshot(cfg)
}
func New() *Screenshoter {
return &Screenshoter{
ScreenshotEngine: engine,
ScreenshotEngine: ScreenshotImplementation{},
}
}
func (s *Screenshoter) Engine() ScreenshotEngine {
return s.ScreenshotEngine
}
// TODO: add the support of Wayland
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(
@@ -158,13 +156,27 @@ func imgFitTo(src image.Image, size image.Point) (image.Image, error) {
factor = math.Min(factor, float64(size.Y)/float64(sizeCur.Y))
newWidth := int(float64(sizeCur.X) * factor)
newHeight := int(float64(sizeCur.Y) * factor)
output := image.NewRGBA(image.Rectangle{Max: image.Point{
newSize := image.Rectangle{Max: image.Point{
X: newWidth,
Y: newHeight,
}})
}}
var output image.Image
switch src := src.(type) {
case *image.RGBA:
output = image.NewRGBA(newSize)
case *image.RGBA64:
output = image.NewRGBA64(newSize)
case *image.Gray:
output = image.NewGray(newSize)
case *image.YCbCr:
output = image.NewYCbCr(newSize, src.SubsampleRatio)
default:
return nil, fmt.Errorf("image format %T is not supported, yet", src)
}
err := rez.Convert(output, src, rez.NewBicubicFilter())
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to convert: %w", err)
}
return output, nil
}
@@ -269,7 +281,7 @@ func (p *Panel) setScreenshot(
screenshot image.Image,
) {
bounds := screenshot.Bounds()
logger.Tracef(ctx, "screenshot bounds: %#+v", bounds)
logger.Tracef(ctx, "screenshot %T bounds: %#+v", screenshot, bounds)
if bounds.Max.X == 0 || bounds.Max.Y == 0 {
p.DisplayError(fmt.Errorf("received an empty screenshot"))
p.screenshoterLocker.Do(ctx, func() {
@@ -313,12 +325,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{},