mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-27 11:52:11 +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/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
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/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=
|
||||||
|
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
|
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)
|
@@ -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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -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{},
|
||||||
|
Reference in New Issue
Block a user