mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-26 19:41:17 +08:00
Add the support of screenshotting using libav
This commit is contained in:
2
go.mod
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
7
pkg/screenshoter/cmd/screenshoter/assert.go
Normal file
7
pkg/screenshoter/cmd/screenshoter/assert.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
func assertNoError(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
50
pkg/screenshoter/cmd/screenshoter/main.go
Normal file
50
pkg/screenshoter/cmd/screenshoter/main.go
Normal 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)
|
||||
}
|
80
pkg/screenshoter/screenshoter_libav.go
Normal file
80
pkg/screenshoter/screenshoter_libav.go
Normal 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)
|
||||
}
|
||||
}
|
@@ -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)
|
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -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{},
|
||||
|
Reference in New Issue
Block a user