blob: 4a369828b8e61775ddd70ab271289eeafbb248b5 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package uiauto
import (
"context"
"image"
"net"
"time"
vnc "github.com/matts1/vnc2video"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/kmsvnc"
"chromiumos/tast/testing"
)
// The worst case ratio between the amount of time required to encode a video and the video duration.
// For example, 0.3 would mean it takes at most 0.3 seconds to encode 1 second of video.
const encodingToVideoRatio = 0.3
const encodingToTestDurationRatio = encodingToVideoRatio / (encodingToVideoRatio + 1)
type videoConfig struct {
fileName string
framerate int
recordOnSuccess bool
}
var defaultConfig = videoConfig{
fileName: "recording.webm",
framerate: 10,
recordOnSuccess: false,
}
// RecordingFileName sets the filename of the video recording (default is recording.webm).
func RecordingFileName(fileName string) func(*videoConfig) {
return func(c *videoConfig) {
c.fileName = fileName
}
}
// RecordingFramerate sets the framerate of the video recording.
func RecordingFramerate(framerate int) func(*videoConfig) {
return func(c *videoConfig) {
c.framerate = framerate
}
}
// RecordOnSuccess ensures that we record video even if the test succeeds.
func RecordOnSuccess() func(*videoConfig) {
return func(c *videoConfig) {
c.recordOnSuccess = true
}
}
// ReserveForVNCRecordingCleanup shortens the context for vnc video encoding.
// It calculates the amount of time required to clean up, and calls
// ctxutil.Shorten by that amount.
func ReserveForVNCRecordingCleanup(ctx context.Context) (context.Context, context.CancelFunc) {
dl, ok := ctx.Deadline()
if !ok {
return context.WithCancel(ctx)
}
timeLeft := time.Until(dl)
encodingTimeRequired := time.Duration(float64(timeLeft.Nanoseconds()) * encodingToTestDurationRatio)
// Allow 4 seconds to kill kmsvnc.
return ctxutil.Shorten(ctx, encodingTimeRequired+4*time.Second)
}
// RecordVNCVideo starts recording video from a VNC stream.
// It returns a function that will stop recording the video.
// Example usage:
// stopRecording := RecordVNCVideo(ctx, s, RecordingFramerate(5))
// defer stopRecording()
func RecordVNCVideo(ctx context.Context, s testingState, mods ...func(*videoConfig)) (stopRecording func()) {
stopRecording, err := RecordVNCVideoCritical(ctx, s, mods...)
if err != nil {
testing.ContextLog(ctx, "Error while starting screen recording: ", err)
return func() {}
}
return stopRecording
}
// RecordVNCVideoCritical starts recording video from a VNC stream.
// If the recording was unable to start, an error will be returned.
// Otherwise, a function to stop and save the recording will be returned.
// If the stop recording function fails, it will be logged, but as it is
// non-critical to the test itself, the test will still pass.
// Example usage:
// stopRecording, err := RecordVNCVideoCritical(ctx, s, RecordingFramerate(5))
// if err != nil {
// handle err
// }
// defer stopRecording()
func RecordVNCVideoCritical(ctx context.Context, s testingState, mods ...func(*videoConfig)) (stopRecording func(), err error) {
cfg := defaultConfig
for _, mod := range mods {
mod(&cfg)
}
kms, err := kmsvnc.NewKmsvnc(ctx, false)
if err != nil {
return nil, err
}
cleanups := []func(){func() {
if err := kms.Stop(ctx); err != nil {
testing.ContextLog(ctx, "Failed to stop kmsvnc: ", err)
}
}}
// This is basically a fake defer, since we can't use defer because
// if all goes well, this gets called when we stop recording, not
// when this function ends.
cleanup := func() {
for i := len(cleanups) - 1; i >= 0; i-- {
cleanups[i]()
}
}
serverMessageCh := make(chan vnc.ServerMessage)
errorCh := make(chan error)
quitCh := make(chan struct{})
enc := vnc.NewTightEncoder()
ccfg := &vnc.ClientConfig{
SecurityHandlers: []vnc.SecurityHandler{
&vnc.ClientAuthNone{},
},
DrawCursor: true,
PixelFormat: vnc.PixelFormat32bit,
ServerMessageCh: serverMessageCh,
Messages: vnc.DefaultServerMessages,
Encodings: []vnc.Encoding{
&enc,
// This encoding isn't actually used, but if you tell it you're handling
// the cursor it won't try and put the cursor into the video stream itself.
&vnc.CursorPseudoEncoding{},
},
ErrorCh: errorCh,
QuitCh: quitCh,
}
nc, err := net.DialTimeout("tcp", "localhost:5900", 5*time.Second)
if err != nil {
cleanup()
return nil, err
}
// Don't add nc.Close to cleanup, since it gets owned by the vnc.Connect.
cc, err := vnc.Connect(ctx, nc, ccfg)
if err != nil {
if errCleanup := nc.Close(); err != nil {
testing.ContextLog(ctx, "Failed to close localhost:5900: ", errCleanup)
}
cleanup()
return nil, errors.Wrap(err, "error connecting to vnc host")
}
cleanups = append(cleanups, func() {
if err := cc.Close(); err != nil {
testing.ContextLog(ctx, "Failed to close VNC connection: ", err)
}
})
if err := cc.SetEncodings([]vnc.EncodingType{
vnc.EncCursorPseudo,
vnc.EncTight,
}); err != nil {
cleanup()
return nil, err
}
startTime := time.Time{}
recordingStart := make(chan struct{})
var errs []error
go func() {
for {
select {
case <-quitCh:
return
case err := <-errorCh:
err = errors.Wrap(err, "received error during screen recording - stopping screen recording and continuing test")
testing.ContextLog(ctx, err.Error())
errs = append(errs, err)
// By returning, we simply ensure that we don't ask for any more frames.
// This way, at the end we will still encode video for the frames we do have.
return
case msg := <-serverMessageCh:
if msg.Type() == vnc.FramebufferUpdateMsgType {
if startTime.Equal(time.Time{}) {
startTime = time.Now()
recordingStart <- struct{}{}
}
// The VNC RFB protocol requires that we finish processing a frame before we request a new one.
reqMsg := vnc.FramebufferUpdateRequest{Inc: 1, X: 0, Y: 0, Width: cc.Width(), Height: cc.Height()}
if err := reqMsg.Write(cc); err != nil {
err = errors.Wrap(err, "failed to request an additional frame")
testing.ContextLog(ctx, err.Error())
errs = append(errs, err)
return
}
}
}
}
}()
select {
case err := <-errorCh:
cleanup()
return nil, err
case <-recordingStart:
testing.ContextLog(ctx, "Started screen recording")
}
return func() {
// VNC stream is slightly behind. Allow a second to capture everything that happened since.
// Potential room for improvement: start encoding video before this (it's only 1 second though).
if err := testing.Sleep(ctx, time.Second*2); err != nil {
errs = append(errs, err)
}
// If the error is halfway through the test, the user won't notice. Put it at the end.
for _, err := range errs {
testing.ContextLog(ctx, "Re-showing previous error in video recording: ", err)
}
// Call cleanup before the function ends so that we stop getting more frames.
cleanup()
if !cfg.recordOnSuccess && !s.HasError() {
return
}
end := time.Now()
testing.ContextLogf(ctx, "starting to encode a %v video recording. This can take a while", end.Sub(startTime))
canvas := image.NewRGBA(image.Rect(0, 0, int(cc.Width()), int(cc.Height())))
if err := createVideo(s, &enc, canvas, startTime, time.Now(), cfg); err != nil {
testing.ContextLog(ctx, "Failed to create video recording. Check above in the log to see if there were other video recording errors: ", err)
} else {
testing.ContextLogf(ctx, "Completed encoding %v of video in %v", end.Sub(startTime), time.Since(end))
}
}, nil
}