mirror of
https://github.com/saily/vnc-recorder.git
synced 2025-04-27 10:20:54 +00:00
First draft
Signed-off-by: Daniel Widerin <daniel@widerin.net>
This commit is contained in:
parent
f9f922e861
commit
b0254ad356
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!*.go
|
||||
!*.toml
|
||||
!*.lock
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
/vendor
|
||||
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM golang:alpine as build-env
|
||||
LABEL maintainer="daniel@widerin.net"
|
||||
|
||||
ENV GOBIN /go/bin
|
||||
|
||||
RUN mkdir /go/src/app && \
|
||||
apk --no-cache add git curl && \
|
||||
curl -sSL https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
|
||||
ADD . /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
|
||||
RUN dep ensure && \
|
||||
go build -o /vnc-recorder .
|
||||
|
||||
FROM jrottenberg/ffmpeg:4.0-alpine
|
||||
COPY --from=build-env /vnc-recorder /
|
||||
ENTRYPOINT ["/vnc-recorder"]
|
||||
CMD [""]
|
66
Gopkg.lock
generated
Normal file
66
Gopkg.lock
generated
Normal file
@ -0,0 +1,66 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "reusable-encoders"
|
||||
digest = "1:c9c2a078726605536f2c71e7a282e9f267dac31ac27379d63fd91bc6215fba84"
|
||||
name = "github.com/amitbet/vnc2video"
|
||||
packages = [
|
||||
".",
|
||||
"encoders",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "22e16ea7e65b4c8682938bed11aab7576c22533f"
|
||||
source = "https://github.com/saily/vnc2video.git"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7644c593fbd72dfeda547b10e0aba09fbce3c53edd45cb9e4b287184ca82d8a4"
|
||||
name = "github.com/icza/mjpeg"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "85dfbe473743c5a48253effd11423ae2d82ac15b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
|
||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b24d38b282bacf9791408a080f606370efa3d364e4b5fd9ba0f7b87786d3b679"
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||
version = "v1.20.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8f5108406bc43c7669b0d67d282e40d05c9f268615fcaf8c1f0f76965aa3f09f"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = "UT"
|
||||
revision = "4c4f7f33c9ed00de01c4c741d2177abfcfe19307"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/amitbet/vnc2video",
|
||||
"github.com/amitbet/vnc2video/encoders",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/urfave/cli",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
43
Gopkg.toml
Normal file
43
Gopkg.toml
Normal file
@ -0,0 +1,43 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.4.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/urfave/cli"
|
||||
version = "1.20.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/amitbet/vnc2video"
|
||||
source = "https://github.com/saily/vnc2video.git"
|
||||
branch = "reusable-encoders"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
61
README.md
61
README.md
@ -1,2 +1,59 @@
|
||||
# vnc-recorder
|
||||
Record VNC screens to mp4 video using ffmpeg
|
||||
# VNC recorder
|
||||
|
||||
> this is wip, don't use in production!
|
||||
|
||||
Record [VNC] screens to mp4 video using [ffmpeg]. Thanks to
|
||||
[amitbet for providing his vnc2video](https://github.com/amitbet/vnc2video)
|
||||
library which made this wrapper possible.
|
||||
|
||||
## Use
|
||||
|
||||
docker run -it saily/vnc-recorder --help
|
||||
|
||||
|
||||
NAME:
|
||||
vnc-recorder - Connect to a vnc server and record the screen to a video.
|
||||
|
||||
USAGE:
|
||||
vnc-recorder [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
1.0
|
||||
|
||||
AUTHOR:
|
||||
Daniel Widerin <daniel@widerin.net>
|
||||
|
||||
COMMANDS:
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--ffmpeg value Which ffmpeg executable to use (default: "ffmpeg") [$VR_FFMPEG_BIN]
|
||||
--host value VNC host (default: "localhost") [$VR_VNC_HOST]
|
||||
--port value VNC port (default: 5900) [$VR_VNC_PORT]
|
||||
--password value Password to connect to the VNC host (default: "secret") [$VR_VNC_PASSWORD]
|
||||
--framerate value Framerate to record (default: 30) [$VR_FRAMERATE]
|
||||
--outfile value Output file to record to. (default: "output.mp4") [$VR_OUTFILE]
|
||||
--help, -h show help
|
||||
--version, -v print the version
|
||||
|
||||
**Note:** If you run vnc-recorder from your command line and don't use [docker]
|
||||
you might want to customize the `--ffmpeg` flag to point to an existing
|
||||
[ffmpeg] installation.
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
docker build -t yourbuild .
|
||||
docker run -it yourbuild --help
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add tests!
|
||||
- [ ] Add more encoder options
|
||||
- [ ] Get some patches merged for our dependencies
|
||||
|
||||
|
||||
[ffmpeg]: https://ffmpeg.org
|
||||
[docker]: https://www.docker.com
|
||||
[vnc]: https://en.wikipedia.org/wiki/Virtual_Network_Computing
|
||||
|
228
main.go
Normal file
228
main.go
Normal file
@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
vnc "github.com/amitbet/vnc2video"
|
||||
"github.com/amitbet/vnc2video/encoders"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = path.Base(os.Args[0])
|
||||
app.Usage = "Connect to a vnc server and record the screen to a video."
|
||||
app.Version = "1.0"
|
||||
app.Authors = []cli.Author{
|
||||
{
|
||||
Name: "Daniel Widerin",
|
||||
Email: "daniel@widerin.net",
|
||||
},
|
||||
}
|
||||
app.Action = recorder
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ffmpeg",
|
||||
Value: "ffmpeg",
|
||||
Usage: "Which ffmpeg executable to use",
|
||||
EnvVar: "VR_FFMPEG_BIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "localhost",
|
||||
Usage: "VNC host",
|
||||
EnvVar: "VR_VNC_HOST",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Value: 5900,
|
||||
Usage: "VNC port",
|
||||
EnvVar: "VR_VNC_PORT",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password",
|
||||
Value: "secret",
|
||||
Usage: "Password to connect to the VNC host",
|
||||
EnvVar: "VR_VNC_PASSWORD",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "framerate",
|
||||
Value: 30,
|
||||
Usage: "Framerate to record",
|
||||
EnvVar: "VR_FRAMERATE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "outfile",
|
||||
Value: "output.mp4",
|
||||
Usage: "Output file to record to.",
|
||||
EnvVar: "VR_OUTFILE",
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func recorder(c *cli.Context) error {
|
||||
address := fmt.Sprintf("%s:%d", c.String("host"), c.Int("port"))
|
||||
nc, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err != nil {
|
||||
log.Fatalf("Error connecting to VNC host. %v", err)
|
||||
return err
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
log.Infof("Connected to %s", address)
|
||||
|
||||
// Negotiate connection with the server.
|
||||
cchServer := make(chan vnc.ServerMessage)
|
||||
cchClient := make(chan vnc.ClientMessage)
|
||||
errorCh := make(chan error)
|
||||
|
||||
ccfg := &vnc.ClientConfig{
|
||||
SecurityHandlers: []vnc.SecurityHandler{
|
||||
// &vnc.ClientAuthATEN{Username: []byte(os.Args[2]), Password: []byte(os.Args[3])}
|
||||
&vnc.ClientAuthVNC{Password: []byte(c.String("password"))},
|
||||
&vnc.ClientAuthNone{},
|
||||
},
|
||||
DrawCursor: true,
|
||||
PixelFormat: vnc.PixelFormat32bit,
|
||||
ClientMessageCh: cchClient,
|
||||
ServerMessageCh: cchServer,
|
||||
Messages: vnc.DefaultServerMessages,
|
||||
Encodings: []vnc.Encoding{
|
||||
&vnc.RawEncoding{},
|
||||
&vnc.TightEncoding{},
|
||||
&vnc.HextileEncoding{},
|
||||
&vnc.ZRLEEncoding{},
|
||||
&vnc.CopyRectEncoding{},
|
||||
&vnc.CursorPseudoEncoding{},
|
||||
&vnc.CursorPosPseudoEncoding{},
|
||||
&vnc.ZLibEncoding{},
|
||||
&vnc.RREEncoding{},
|
||||
},
|
||||
ErrorCh: errorCh,
|
||||
}
|
||||
|
||||
cc, err := vnc.Connect(context.Background(), nc, ccfg)
|
||||
defer cc.Close()
|
||||
|
||||
screenImage := cc.Canvas
|
||||
if err != nil {
|
||||
log.Fatalf("Error negotiating connection to VNC host. %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ffmpeg_path, err := exec.LookPath(c.String("ffmpeg"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Infof("Using %s for encoding", ffmpeg_path)
|
||||
vcodec := &encoders.Encoder{
|
||||
BinPath: ffmpeg_path,
|
||||
Framerate: c.Int("framerate"),
|
||||
Cmd: exec.Command(ffmpeg_path,
|
||||
"-f", "image2pipe",
|
||||
"-vcodec", "ppm",
|
||||
"-r", strconv.Itoa(c.Int("framerate")),
|
||||
"-an", // no audio
|
||||
"-y",
|
||||
"-i", "-",
|
||||
"-vcodec", "libx264", //"libvpx",//"libvpx-vp9"//"libx264"
|
||||
"-preset", "fast",
|
||||
"-crf", "24",
|
||||
c.String("outfile"),
|
||||
),
|
||||
}
|
||||
|
||||
go vcodec.Run()
|
||||
|
||||
for _, enc := range ccfg.Encodings {
|
||||
myRenderer, ok := enc.(vnc.Renderer)
|
||||
|
||||
if ok {
|
||||
myRenderer.SetTargetImage(screenImage)
|
||||
}
|
||||
}
|
||||
|
||||
cc.SetEncodings([]vnc.EncodingType{
|
||||
vnc.EncCursorPseudo,
|
||||
vnc.EncPointerPosPseudo,
|
||||
vnc.EncCopyRect,
|
||||
vnc.EncTight,
|
||||
vnc.EncZRLE,
|
||||
vnc.EncHextile,
|
||||
vnc.EncZlib,
|
||||
vnc.EncRRE,
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
timeStart := time.Now()
|
||||
|
||||
vcodec.Encode(screenImage.Image)
|
||||
|
||||
timeTarget := timeStart.Add((1000 / time.Duration(vcodec.Framerate)) * time.Millisecond)
|
||||
timeLeft := timeTarget.Sub(time.Now())
|
||||
if timeLeft > 0 {
|
||||
time.Sleep(timeLeft)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT, )
|
||||
frameBufferReq := 0
|
||||
timeStart := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errorCh:
|
||||
panic(err)
|
||||
case msg := <-cchClient:
|
||||
log.Debugf("Received client message type:%v msg:%v\n", msg.Type(), msg)
|
||||
case msg := <-cchServer:
|
||||
if msg.Type() == vnc.FramebufferUpdateMsgType {
|
||||
secsPassed := time.Now().Sub(timeStart).Seconds()
|
||||
frameBufferReq++
|
||||
reqPerSec := float64(frameBufferReq) / secsPassed
|
||||
//counter++
|
||||
//jpeg.Encode(out, screenImage, nil)
|
||||
///vcodec.Encode(screenImage)
|
||||
log.Debugf("reqs=%d, seconds=%f, Req Per second= %f", frameBufferReq, secsPassed, reqPerSec)
|
||||
|
||||
reqMsg := vnc.FramebufferUpdateRequest{Inc: 1, X: 0, Y: 0, Width: cc.Width(), Height: cc.Height()}
|
||||
//cc.ResetAllEncodings()
|
||||
reqMsg.Write(cc)
|
||||
}
|
||||
case signal := <-sigc:
|
||||
if signal != nil {
|
||||
log.Info(signal, " received, exit.")
|
||||
vcodec.Close()
|
||||
// give some time to write the file
|
||||
time.Sleep(time.Second * 5)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user