First draft

Signed-off-by: Daniel Widerin <daniel@widerin.net>
This commit is contained in:
Daniel Widerin 2019-06-02 12:32:31 +02:00
parent f9f922e861
commit b0254ad356
No known key found for this signature in database
GPG Key ID: 1075749274B44FE9
7 changed files with 420 additions and 2 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
*
!*.go
!*.toml
!*.lock

1
.gitignore vendored
View File

@ -10,3 +10,4 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
/vendor

19
Dockerfile Normal file
View 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
View 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
View 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

View File

@ -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
View 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
}