mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-31 05:19:38 +08:00
feat: add adb screen record
This commit is contained in:
@@ -3,6 +3,7 @@ package uixt
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
@@ -779,54 +780,105 @@ func (ad *ADBDriver) GetIme() (ime string, err error) {
|
||||
return currentIme, nil
|
||||
}
|
||||
|
||||
func (ad *ADBDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
||||
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
||||
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||
func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||
options := option.NewActionOptions(opts...)
|
||||
|
||||
file, err := os.Create(fileName)
|
||||
var filePath string
|
||||
if options.ScreenRecordPath != "" {
|
||||
filePath = options.ScreenRecordPath
|
||||
} else {
|
||||
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
||||
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||
}
|
||||
|
||||
duration := options.Duration
|
||||
audioOn := options.ScreenRecordWithAudio
|
||||
|
||||
// get android system version
|
||||
var sysVersion int
|
||||
if systemVersion, err := ad.Device.SystemVersion(); err == nil {
|
||||
if version, err := strconv.Atoi(systemVersion); err == nil {
|
||||
sysVersion = version
|
||||
}
|
||||
}
|
||||
if sysVersion == 0 {
|
||||
log.Warn().Err(err).Msg("get android system version failed")
|
||||
}
|
||||
|
||||
var useAdbScreenRecord bool
|
||||
if !audioOn {
|
||||
log.Info().Bool("audioOn", audioOn).Msg("screen record with adb screenrecord by default")
|
||||
useAdbScreenRecord = true
|
||||
} else if sysVersion != 0 && sysVersion < 11 {
|
||||
// scrcpy audio forwarding is supported for devices with Android 11 or higher
|
||||
// https://github.com/Genymobile/scrcpy/blob/master/doc/audio.md
|
||||
log.Warn().Bool("audioOn", audioOn).Int("version", sysVersion).
|
||||
Msg("Audio disabled, it is only supported for Android >= 11, use adb screenrecord")
|
||||
useAdbScreenRecord = true
|
||||
}
|
||||
|
||||
if useAdbScreenRecord {
|
||||
res, err := ad.Device.ScreenRecord(duration)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "screen record failed")
|
||||
}
|
||||
if err := os.WriteFile(filePath, res, 0o644); err != nil {
|
||||
return "", errors.Wrap(err, "write screen record file failed")
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// screen record with audio
|
||||
log.Info().Msg("screen record with audio, use scrcpy")
|
||||
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
log.Error().Err(err)
|
||||
return "", err
|
||||
return "", errors.Wrap(err, "create screen record file failed")
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
// scrcpy -s 7d21bb91 --record=file.mp4 -N
|
||||
// start scrcpy
|
||||
cmd := exec.Command(
|
||||
"scrcpy",
|
||||
"-s", ad.Device.Serial(),
|
||||
fmt.Sprintf("--record=%s", fileName),
|
||||
"-N",
|
||||
fmt.Sprintf("--record=%s", filePath),
|
||||
"--record-format=mp4",
|
||||
"--max-fps=30",
|
||||
"--no-playback", // Disable video and audio playback on the computer
|
||||
)
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
// 启动命令
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Error().Err(err)
|
||||
return "", err
|
||||
}
|
||||
timer := time.After(duration)
|
||||
|
||||
done := make(chan error)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", errors.Wrap(err, "start screen record failed")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(duration*float64(time.Second)))
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
// 等待 ffmpeg 命令执行完毕
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-timer:
|
||||
// 超时,停止 scrcpy 进程
|
||||
case <-ctx.Done():
|
||||
// 超时,优雅停止 scrcpy 进程
|
||||
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
|
||||
log.Error().Err(err)
|
||||
log.Error().Err(err).Msg("failed to stop scrcpy process")
|
||||
_ = cmd.Process.Kill() // 强制结束进程
|
||||
}
|
||||
<-done // 等待进程完全退出
|
||||
case err := <-done:
|
||||
// ffmpeg 正常结束
|
||||
if err != nil {
|
||||
log.Error().Err(err)
|
||||
return "", err
|
||||
return "", errors.Wrap(err, "screen record failed")
|
||||
}
|
||||
}
|
||||
return filepath.Abs(fileName)
|
||||
|
||||
return filepath.Abs(filePath)
|
||||
}
|
||||
|
||||
func (ad *ADBDriver) Setup() error {
|
||||
|
||||
@@ -195,7 +195,7 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) {
|
||||
|
||||
func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
||||
driver := setupADBDriverExt(t)
|
||||
path, err := driver.ScreenRecord(5 * time.Second)
|
||||
path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(path)
|
||||
t.Log(path)
|
||||
|
||||
@@ -614,7 +614,7 @@ func (wd *BrowserDriver) GetSession() *DriverSession {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wd *BrowserDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
||||
func (wd *BrowserDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
@@ -37,7 +36,7 @@ type IDriver interface {
|
||||
ForegroundInfo() (app types.AppInfo, err error)
|
||||
WindowSize() (types.Size, error)
|
||||
ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, error)
|
||||
ScreenRecord(duration time.Duration) (videoPath string, err error)
|
||||
ScreenRecord(opts ...option.ActionOption) (videoPath string, err error)
|
||||
Source(srcOpt ...option.SourceOption) (string, error)
|
||||
Orientation() (orientation types.Orientation, err error)
|
||||
Rotation() (rotation types.Rotation, err error)
|
||||
|
||||
@@ -273,7 +273,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
case ACTION_ScreenShot:
|
||||
// take screenshot
|
||||
log.Info().Msg("take screenshot for current screen")
|
||||
_, err := dExt.GetScreenResult(action.GetScreenOptions()...)
|
||||
_, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
|
||||
return err
|
||||
case ACTION_ClosePopups:
|
||||
return dExt.ClosePopupsHandler()
|
||||
|
||||
@@ -364,11 +364,11 @@ func (s *StubIOSDriver) WindowSize() (types.Size, error) {
|
||||
return s.WDADriver.WindowSize()
|
||||
}
|
||||
|
||||
func (s *StubIOSDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
||||
func (s *StubIOSDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||
if err := s.SetupWda(); err != nil {
|
||||
return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||||
}
|
||||
return s.WDADriver.ScreenRecord(duration)
|
||||
return s.WDADriver.ScreenRecord(opts...)
|
||||
}
|
||||
|
||||
func (s *StubIOSDriver) Orientation() (types.Orientation, error) {
|
||||
|
||||
@@ -253,7 +253,7 @@ func (hd *HDCDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
return hd.points, nil
|
||||
}
|
||||
|
||||
func (hd *HDCDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
||||
func (hd *HDCDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
|
||||
@@ -875,10 +875,13 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
|
||||
return wd.Session.POST(data, "/gtf/automation/log")
|
||||
}
|
||||
|
||||
func (wd *WDADriver) ScreenRecord(duration time.Duration) (videoPath string, err error) {
|
||||
func (wd *WDADriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
|
||||
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
|
||||
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||
|
||||
options := option.NewActionOptions(opts...)
|
||||
duration := time.Duration(options.Duration * float64(time.Second))
|
||||
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating file:", err)
|
||||
|
||||
@@ -302,7 +302,7 @@ func TestDriver_WDA_AccessibleSource(t *testing.T) {
|
||||
|
||||
func TestDriver_WDA_ScreenRecord(t *testing.T) {
|
||||
driver := setupWDADriverExt(t)
|
||||
path, err := driver.ScreenRecord(5 * time.Second)
|
||||
path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
|
||||
assert.Nil(t, err)
|
||||
t.Log(path)
|
||||
}
|
||||
|
||||
@@ -120,11 +120,10 @@ func (o *ActionOptions) Options() []ActionOption {
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
options = append(options, o.GetScreenShotOptions()...)
|
||||
options = append(options, o.GetScreenRecordOptions()...)
|
||||
|
||||
func (o *ActionOptions) GetScreenOptions() []ActionOption {
|
||||
return o.ScreenOptions.Options()
|
||||
return options
|
||||
}
|
||||
|
||||
func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import "github.com/httprunner/httprunner/v5/uixt/types"
|
||||
|
||||
type ScreenOptions struct {
|
||||
ScreenShotOptions
|
||||
ScreenRecordOptions
|
||||
ScreenFilterOptions
|
||||
}
|
||||
|
||||
@@ -18,7 +19,7 @@ type ScreenShotOptions struct {
|
||||
ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"`
|
||||
}
|
||||
|
||||
func (o *ScreenShotOptions) Options() []ActionOption {
|
||||
func (o *ScreenShotOptions) GetScreenShotOptions() []ActionOption {
|
||||
options := make([]ActionOption, 0)
|
||||
if o == nil {
|
||||
return options
|
||||
@@ -125,6 +126,49 @@ func WithScreenShotFileName(fileName string) ActionOption {
|
||||
}
|
||||
}
|
||||
|
||||
type ScreenRecordOptions struct {
|
||||
ScreenRecordDuration float64 `json:"screenrecord_duration,omitempty" yaml:"screenrecord_duration,omitempty"`
|
||||
ScreenRecordWithAudio bool `json:"screenrecord_with_audio,omitempty" yaml:"screenrecord_with_audio,omitempty"`
|
||||
ScreenRecordPath string `json:"screenrecord_path,omitempty" yaml:"screenrecord_path,omitempty"`
|
||||
}
|
||||
|
||||
func (o *ScreenRecordOptions) GetScreenRecordOptions() []ActionOption {
|
||||
options := make([]ActionOption, 0)
|
||||
if o == nil {
|
||||
return options
|
||||
}
|
||||
|
||||
// screen record options
|
||||
if o.ScreenRecordDuration > 0 {
|
||||
options = append(options, WithDuration(o.ScreenRecordDuration))
|
||||
}
|
||||
if o.ScreenRecordWithAudio {
|
||||
options = append(options, WithScreenRecordAudio(true))
|
||||
}
|
||||
if o.ScreenRecordPath != "" {
|
||||
options = append(options, WithScreenRecordPath(o.ScreenRecordPath))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func WithScreenRecordDuation(duration float64) ActionOption {
|
||||
return func(o *ActionOptions) {
|
||||
o.ScreenRecordDuration = duration
|
||||
}
|
||||
}
|
||||
|
||||
func WithScreenRecordAudio(audioOn bool) ActionOption {
|
||||
return func(o *ActionOptions) {
|
||||
o.ScreenRecordWithAudio = audioOn
|
||||
}
|
||||
}
|
||||
|
||||
func WithScreenRecordPath(path string) ActionOption {
|
||||
return func(o *ActionOptions) {
|
||||
o.ScreenRecordPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner
|
||||
// [x1, y1, x2, y2] in percentage of the screen
|
||||
type Scope []float64
|
||||
|
||||
Reference in New Issue
Block a user