Skip to content

Commit 3a8188a

Browse files
committed
Add the support of screenshotting using libav
1 parent 30db31f commit 3a8188a

8 files changed

Lines changed: 163 additions & 19 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ require (
298298
github.com/spf13/pflag v1.0.6
299299
github.com/stretchr/testify v1.10.0
300300
github.com/xaionaro-go/audio v0.0.0-20250210102901-abfced9d5ef3
301-
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18
301+
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2
302302
github.com/xaionaro-go/datacounter v1.0.4
303303
github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42
304304
github.com/xaionaro-go/grpcproxy v0.0.0-20241103205849-a8fef42e72f9

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,10 @@ github.com/xaionaro-go/avmediacodec v0.0.0-20250505012527-c819676502d8 h1:FZn9+T
10781078
github.com/xaionaro-go/avmediacodec v0.0.0-20250505012527-c819676502d8/go.mod h1:2W2Kp/HJFXcFBppQ4YytgDy/ydFL3hGc23xSB1U/Luc=
10791079
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18 h1:OfkJnBBNbr3AUbqumbGpY78W8FoEHaz0zKdKzZMmkGc=
10801080
github.com/xaionaro-go/avpipeline v0.0.0-20250727184631-f13f8d149b18/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
1081+
github.com/xaionaro-go/avpipeline v0.0.0-20250808224324-1125ac2d144b h1:J+fc4WXTCnxc1LfafpRndcWOjq0NDCpToc8NPBHdSIQ=
1082+
github.com/xaionaro-go/avpipeline v0.0.0-20250808224324-1125ac2d144b/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
1083+
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2 h1:xKetGyk2/9XGZwiupCYGJLoAkw2dl774Gp4afxLbZoY=
1084+
github.com/xaionaro-go/avpipeline v0.0.0-20250809004114-8edb93a58cf2/go.mod h1:eFxxNA50Pyp1B+snK0TlmpsnrGUtzJohbY/+J3BEvqA=
10811085
github.com/xaionaro-go/datacounter v1.0.4 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
10821086
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
10831087
github.com/xaionaro-go/eventbus v0.0.0-20250720144534-4670758005d9 h1:ZAm8ueMw5D85LDeV1Kboc3ANqXr3LK/eXIl9hj1BJyM=
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
func assertNoError(err error) {
4+
if err != nil {
5+
panic(err)
6+
}
7+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"image"
8+
"time"
9+
10+
"github.com/xaionaro-go/streamctl/pkg/screenshot"
11+
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
12+
)
13+
14+
func main() {
15+
xMin := flag.Int("x-min", 0, "")
16+
xMax := flag.Int("x-max", 100, "")
17+
yMin := flag.Int("y-min", 0, "")
18+
yMax := flag.Int("y-max", 100, "")
19+
fps := flag.Float64("fps", 30, "")
20+
flag.Parse()
21+
22+
ctx := context.Background()
23+
24+
h := screenshoter.New()
25+
26+
startedAt := time.Now()
27+
frameCount := 0
28+
err := h.Loop(
29+
ctx,
30+
time.Duration(float64(time.Second) / *fps),
31+
screenshot.Config{
32+
Bounds: image.Rectangle{
33+
Min: image.Point{
34+
X: *xMin,
35+
Y: *yMin,
36+
},
37+
Max: image.Point{
38+
X: *xMax,
39+
Y: *yMax,
40+
},
41+
},
42+
},
43+
func(context.Context, image.Image) {
44+
frameCount++
45+
fps := float64(frameCount) / time.Since(startedAt).Seconds()
46+
fmt.Printf("received a picture; overall FPS: %f\n", fps)
47+
},
48+
)
49+
assertNoError(err)
50+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//go:build with_libav && linux
2+
// +build with_libav,linux
3+
4+
package screenshoter
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"image"
10+
"time"
11+
12+
"github.com/asticode/go-astiav"
13+
"github.com/facebookincubator/go-belt/tool/logger"
14+
"github.com/xaionaro-go/avpipeline/frame"
15+
"github.com/xaionaro-go/avpipeline/node"
16+
"github.com/xaionaro-go/avpipeline/preset/screencapturer"
17+
"github.com/xaionaro-go/observability"
18+
"github.com/xaionaro-go/streamctl/pkg/screenshot"
19+
)
20+
21+
type Screenshoter struct{}
22+
23+
func New() *Screenshoter {
24+
return &Screenshoter{}
25+
}
26+
27+
func (s *Screenshoter) Loop(
28+
ctx context.Context,
29+
interval time.Duration,
30+
config screenshot.Config,
31+
callback func(context.Context, image.Image),
32+
) error {
33+
fpsFloat := 1 / interval.Seconds()
34+
fps := astiav.Rational{}
35+
fps.SetNum(int(fpsFloat * 1000))
36+
fps.SetDen(1000)
37+
38+
logger.Debugf(ctx, "FPS == %f", fps.Float64())
39+
screenCapturer, err := screencapturer.New(ctx, screencapturer.Params{
40+
Area: config.Bounds,
41+
FPS: fps,
42+
})
43+
if err != nil {
44+
return fmt.Errorf("unable to initialize a screen capturer: %w", err)
45+
}
46+
47+
errCh := make(chan node.Error, 100)
48+
observability.Go(ctx, func(ctx context.Context) {
49+
screenCapturer.Serve(ctx, node.ServeConfig{}, errCh)
50+
})
51+
defer screenCapturer.Close(ctx)
52+
53+
var img image.Image
54+
for {
55+
var frame frame.Output
56+
select {
57+
case <-ctx.Done():
58+
return ctx.Err()
59+
case err := <-errCh:
60+
return fmt.Errorf("unable to serve the screenshoter node: %w", err)
61+
case pkt := <-screenCapturer.OutputPacketChan():
62+
return fmt.Errorf("receive a packet (of size %d), while expected a decoded frame", pkt.GetSize())
63+
case frame = <-screenCapturer.OutputFrameChan():
64+
}
65+
66+
if img == nil {
67+
img, err = frame.Data().GuessImageFormat()
68+
if err != nil {
69+
return fmt.Errorf("unable to guess the image format: %w", err)
70+
}
71+
}
72+
73+
err := frame.Data().ToImage(img)
74+
if err != nil {
75+
return fmt.Errorf("unable to obtain the image: %w", err)
76+
}
77+
78+
callback(ctx, img)
79+
}
80+
}
Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//go:build !with_libav || !linux
2+
// +build !with_libav !linux
3+
14
package screenshoter
25

36
import (
@@ -10,37 +13,37 @@ import (
1013
)
1114

1215
type ScreenshotEngine interface {
13-
Screenshot(cfg screenshot.Config) (*image.RGBA, error)
16+
Screenshot(cfg screenshot.Config) (image.Image, error)
1417
}
1518

1619
type Screenshoter struct {
1720
ScreenshotEngine ScreenshotEngine
1821
}
1922

20-
func New(
21-
engine ScreenshotEngine,
22-
) *Screenshoter {
23-
return &Screenshoter{
24-
ScreenshotEngine: engine,
25-
}
23+
type ScreenshotImplementation struct{}
24+
25+
func (ScreenshotImplementation) Screenshot(cfg screenshot.Config) (image.Image, error) {
26+
return screenshot.Implementation{}.Screenshot(cfg)
2627
}
2728

28-
func (s *Screenshoter) Engine() ScreenshotEngine {
29-
return s.ScreenshotEngine
29+
func New() *Screenshoter {
30+
return &Screenshoter{
31+
ScreenshotEngine: ScreenshotImplementation{},
32+
}
3033
}
3134

3235
func (s *Screenshoter) Loop(
3336
ctx context.Context,
3437
interval time.Duration,
3538
config screenshot.Config,
36-
callback func(context.Context, *image.RGBA),
37-
) {
39+
callback func(context.Context, image.Image),
40+
) error {
3841
t := time.NewTicker(interval)
3942
defer t.Stop()
4043
for {
4144
select {
4245
case <-ctx.Done():
43-
return
46+
return ctx.Err()
4447
case <-t.C:
4548
}
4649
img, err := s.ScreenshotEngine.Screenshot(config)

pkg/streampanel/image.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,18 @@ import (
1717
"github.com/facebookincubator/go-belt/tool/logger"
1818
"github.com/xaionaro-go/observability"
1919
"github.com/xaionaro-go/streamctl/pkg/screenshot"
20-
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
2120
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
2221
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
2322
"github.com/xaionaro-go/xsync"
2423
)
2524

2625
type Screenshoter interface {
27-
Engine() screenshoter.ScreenshotEngine
2826
Loop(
2927
ctx context.Context,
3028
interval time.Duration,
3129
config screenshot.Config,
3230
callback func(context.Context, *image.RGBA),
33-
)
31+
) error
3432
}
3533

3634
func (p *Panel) setImage(
@@ -313,12 +311,15 @@ func (p *Panel) reinitScreenshoter(ctx context.Context) {
313311
ctx, cancelFunc := context.WithCancel(ctx)
314312
p.screenshoterClose = cancelFunc
315313
observability.Go(ctx, func(ctx context.Context) {
316-
p.Screenshoter.Loop(
314+
err := p.Screenshoter.Loop(
317315
ctx,
318316
200*time.Millisecond,
319317
p.Config.Screenshot.Config,
320318
func(ctx context.Context, img *image.RGBA) { p.setScreenshot(ctx, img) },
321319
)
320+
if err != nil {
321+
logger.Errorf(ctx, "unable to run the screenshoter loop: %v", err)
322+
}
322323
})
323324
})
324325
}

pkg/streampanel/panel.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"github.com/xaionaro-go/streamctl/pkg/command"
3535
gconsts "github.com/xaionaro-go/streamctl/pkg/consts"
3636
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
37-
"github.com/xaionaro-go/streamctl/pkg/screenshot"
3837
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
3938
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
4039
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
@@ -204,7 +203,7 @@ func New(
204203
p := &Panel{
205204
configPath: configPath,
206205
Config: Options(opts).ApplyOverrides(cfg),
207-
Screenshoter: screenshoter.New(screenshot.Implementation{}),
206+
Screenshoter: screenshoter.New(),
208207
imageLastDownloaded: map[consts.ImageID][]byte{},
209208
imageLastParsed: map[consts.ImageID]image.Image{},
210209
streamStatus: map[streamcontrol.PlatformName]*streamStatus{},

0 commit comments

Comments
 (0)