blob: efca438cbf78845c414e3fbb9dfa011a50d25e83 [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 camera
import (
"context"
"fmt"
"image"
"io/ioutil"
"os"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/camera/cca"
"chromiumos/tast/local/camera/testutil"
"chromiumos/tast/local/chrome"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: CCAUIPTZ,
Desc: "Opens CCA and verifies the PTZ functionality",
Contacts: []string{"inker@chromium.org", "chromeos-camera-eng@google.com"},
Attr: []string{"group:mainline", "informational", "group:camera-libcamera"},
SoftwareDeps: []string{"camera_app", "chrome"},
Data: []string{"cca_ui.js"},
})
}
const (
y4mWidth = 1280
y4mHeight = 720
patternWidth = 101
patternHeight = 101
)
// preparePattern prepares fake preview y4m file.
func preparePattern() (_ string, retErr error) {
file, err := ioutil.TempFile(os.TempDir(), "*.y4m")
if err != nil {
return "", err
}
defer func() {
if retErr != nil {
os.Remove(file.Name())
}
}()
header := fmt.Sprintf("YUVMPEG2 W%d H%d F30:1 Ip A0:0 C420jpeg\nFRAME\n", y4mWidth, y4mHeight)
if _, err := file.WriteString(header); err != nil {
return "", errors.Wrap(err, "failed to write header of temp y4m")
}
// White background.
const (
bgY = 255
bgU = 128
bgV = 128
)
// Y plane.
yp := make([][]byte, y4mHeight)
for y := range yp {
yp[y] = make([]byte, y4mWidth)
for x := range yp[y] {
yp[y][x] = bgY
}
}
// Draws black square pattern at the center.
cy := y4mHeight / 2
cx := y4mWidth / 2
for dy := -patternHeight / 2; dy <= patternHeight/2; dy++ {
for dx := -patternWidth / 2; dx <= patternWidth/2; dx++ {
yp[cy+dy][cx+dx] = 0
}
}
for _, bs := range yp {
if _, err := file.Write(bs); err != nil {
return "", errors.Wrap(err, "failed to write Y plane of temp y4m")
}
}
// U plane.
up := make([]byte, y4mWidth*y4mHeight/4)
for x := 0; x < len(up); x++ {
up[x] = bgU
}
if _, err := file.Write(up); err != nil {
return "", errors.Wrap(err, "failed to write U plane of temp y4m")
}
// V plane.
vp := make([]byte, y4mWidth*y4mHeight/4)
for x := 0; x < len(vp); x++ {
vp[x] = bgV
}
if _, err := file.Write(vp); err != nil {
return "", errors.Wrap(err, "failed to write V plane of temp y4m")
}
if err := os.Chmod(file.Name(), 0644); err != nil {
return "", err
}
return file.Name(), nil
}
// findPattern finds the region where the pattern resides.
func findPattern(ctx context.Context, app *cca.App) (*image.Rectangle, error) {
frame, err := app.PreviewFrame(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get preview frame")
}
defer frame.Release(ctx)
// Find the coordinates of top-left corner.
minPt, err := frame.Find(ctx, &cca.FirstBlack)
if err != nil {
return nil, err
}
// Find the coordinates of bottom-right corner.
maxPt, err := frame.Find(ctx, &cca.LastBlack)
if err != nil {
return nil, err
}
return &image.Rectangle{*minPt, *maxPt}, nil
}
// calShift checks the size and calculates the x-y shift between |r| and |r2|.
func calShift(r, r2 *image.Rectangle) (*image.Point, error) {
abs := func(n int) int {
if n < 0 {
return -n
}
return n
}
// The pattern should only shift without resizing.
sz := r.Size()
sz2 := r2.Size()
// Do all comparisons with 1px precision tolerance introduced by fake
// file VCD bilinear resizing implementation.
const precision = 1
if abs(sz.X-sz2.X) > precision {
return nil, errors.Errorf("inconsistent width, got %v; want %v", sz2.X, sz.X)
}
if abs(sz.Y-sz2.Y) > precision {
return nil, errors.Errorf("inconsistent height, got %v; want %v", sz2.Y, sz.Y)
}
return &image.Point{r2.Min.X - r.Min.X, r2.Min.Y - r.Min.Y}, nil
}
type ptzControl struct {
// ui is the UI toggled for moving preview in one of PTZ direction.
ui *cca.UIComponent
// testFunc tests pattern before and after ptz control applied moving in the target direction.
testFunc func(r, r2 *image.Rectangle) (bool, error)
}
var (
panLeft = ptzControl{&cca.PanLeftButton, func(r, r2 *image.Rectangle) (bool, error) {
shift, err := calShift(r, r2)
if err != nil {
return false, err
}
return shift.X < 0 && shift.Y == 0, nil
}}
panRight = ptzControl{&cca.PanRightButton, func(r, r2 *image.Rectangle) (bool, error) {
shift, err := calShift(r, r2)
if err != nil {
return false, err
}
return shift.X > 0 && shift.Y == 0, nil
}}
tiltDown = ptzControl{&cca.TiltDownButton, func(r, r2 *image.Rectangle) (bool, error) {
shift, err := calShift(r, r2)
if err != nil {
return false, err
}
return shift.X == 0 && shift.Y < 0, nil
}}
tiltUp = ptzControl{&cca.TiltUpButton, func(r, r2 *image.Rectangle) (bool, error) {
shift, err := calShift(r, r2)
if err != nil {
return false, err
}
return shift.X == 0 && shift.Y > 0, nil
}}
zoomIn = ptzControl{&cca.ZoomInButton, func(r, r2 *image.Rectangle) (bool, error) {
return r.Size().X < r2.Size().X && r.Size().Y < r2.Size().Y, nil
}}
zoomOut = ptzControl{&cca.ZoomOutButton, func(r, r2 *image.Rectangle) (bool, error) {
return r.Size().X > r2.Size().X && r.Size().Y > r2.Size().Y, nil
}}
)
// testToggle tests toggling the control |ctrl|.
func (ctrl *ptzControl) testToggle(ctx context.Context, app *cca.App) error {
pRect, err := findPattern(ctx, app)
if err != nil {
return errors.Wrapf(err, "failed to find pattern before clicking %v: %v", ctrl.ui.Name, err)
}
if err := app.ClickPTZButton(ctx, *ctrl.ui); err != nil {
return errors.Wrapf(err, "failed to click: %v", err)
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
rect, err := findPattern(ctx, app)
if err != nil {
return errors.Wrapf(err, "failed to find pattern after clicking %v: %v", ctrl.ui.Name, err)
}
result, err := ctrl.testFunc(pRect, rect)
if err != nil {
return testing.PollBreak(err)
}
if result {
return nil
}
return errors.Errorf("failed on testing UI with region before %v ; after %v", pRect, rect)
}, &testing.PollOptions{Interval: time.Second}); err != nil {
return errors.Wrapf(err, "failed to run %v test func: %v", ctrl.ui.Name, err)
}
return nil
}
func CCAUIPTZ(ctx context.Context, s *testing.State) {
y4m, err := preparePattern()
if err != nil {
s.Fatal("Failed to prepare temp y4m: ", y4m)
}
defer os.Remove(y4m)
cr, err := chrome.New(ctx, chrome.ExtraArgs(
"--use-fake-device-for-media-stream=fps=30",
"--use-file-for-fake-video-capture="+y4m))
if err != nil {
s.Fatalf("Failed to start chrome with file source %v: %v", y4m, err)
}
defer cr.Close(ctx)
tb, err := testutil.NewTestBridge(ctx, cr, testutil.UseRealCamera)
if err != nil {
s.Fatal("Failed to construct test bridge: ", err)
}
defer tb.TearDown(ctx)
if err := cca.ClearSavedDirs(ctx, cr); err != nil {
s.Fatal("Failed to clear saved directory: ", err)
}
app, err := cca.New(ctx, cr, []string{s.DataPath("cca_ui.js")}, s.OutDir(), tb)
if err != nil {
s.Fatal("Failed to open CCA: ", err)
}
defer func(ctx context.Context) {
if err := app.Close(ctx); err != nil {
s.Error("Failed to close app: ", err)
}
}(ctx)
if err := app.Click(ctx, cca.OpenPTZPanelButton); err != nil {
s.Fatal("Failed to open ptz panel: ", err)
}
// Check cannot pan/tilt when zoom at initial level 0.
for _, control := range []ptzControl{
zoomOut,
panLeft,
panRight,
tiltUp,
tiltDown,
} {
disabled, err := app.Disabled(ctx, *control.ui)
if err != nil {
s.Fatalf("Failed to get disabled state of %v: %v", control.ui.Name, err)
}
if !disabled {
s.Fatalf("UI %v is not disabled at initial zoom level", control.ui.Name)
}
}
// Test move all controls. The controls need to be tested in order such
// that |zoomIn| before all other controls(For all other controls will
// be disabled in minimal zoom level as behavior of digital zoom
// camera), |panLeft| before |panRight| (For the initial pan level is 0
// with range [0, 15]) with initial mirror state, |tiltDown| before
// |tiltUp| (For the initial tilt level is 0 with range[0, 8]).
for _, control := range []ptzControl{
zoomIn,
panLeft,
panRight,
tiltDown,
tiltUp,
zoomOut,
} {
if err := control.testToggle(ctx, app); err != nil {
s.Fatal("Failed: ", err)
}
}
// Check cannot pan/tilt when zoom reset to initial level 0.
if err := app.Click(ctx, cca.PTZResetAllButton); err != nil {
s.Fatal("Failed to reset ptz: ", err)
}
for _, control := range []ptzControl{
zoomOut,
panLeft,
panRight,
tiltUp,
tiltDown,
} {
if err := app.WaitForDisabled(ctx, *control.ui, true); err != nil {
s.Fatalf("Failed to wait for ui %v disabled : %v", control.ui.Name, err)
}
}
}