feat: add adb screen record

This commit is contained in:
lilong.129
2025-03-06 16:57:51 +08:00
parent 5b503a4394
commit cc81c00a82
14 changed files with 159 additions and 43 deletions
+1 -1
View File
@@ -1 +1 @@
v5.0.0-beta-2503061328 v5.0.0-beta-2503061657
+21 -2
View File
@@ -745,7 +745,7 @@ func (d *Device) ScreenCap() ([]byte, error) {
time.Now().Unix()) time.Now().Unix())
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath) _, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "screencap failed")
} }
// remove temp file // remove temp file
@@ -755,5 +755,24 @@ func (d *Device) ScreenCap() ([]byte, error) {
buffer := bytes.NewBuffer(nil) buffer := bytes.NewBuffer(nil)
err = d.Pull(tempPath, buffer) err = d.Pull(tempPath, buffer)
return buffer.Bytes(), err if err != nil {
return nil, errors.Wrap(err, "pull video failed")
}
return buffer.Bytes(), nil
}
func (d *Device) ScreenRecord(seconds float64) ([]byte, error) {
videoPath := fmt.Sprintf("/sdcard/screenrecord_%d.mp4", time.Now().Unix())
_, err := d.RunShellCommandWithBytes("screenrecord",
"--time-limit", fmt.Sprintf("%.1f", seconds), videoPath)
if err != nil {
return nil, errors.Wrap(err, "screenrecord failed")
}
buffer := bytes.NewBuffer(nil)
err = d.Pull(videoPath, buffer)
if err != nil {
return nil, errors.Wrap(err, "pull video failed")
}
return buffer.Bytes(), nil
} }
+1 -1
View File
@@ -37,7 +37,7 @@ func (r *Router) screenResultHandler(c *gin.Context) {
var actionOptions []option.ActionOption var actionOptions []option.ActionOption
if screenReq.Options != nil { if screenReq.Options != nil {
actionOptions = screenReq.Options.Options() actionOptions = screenReq.Options.GetScreenShotOptions()
} }
screenResult, err := driver.GetScreenResult(actionOptions...) screenResult, err := driver.GetScreenResult(actionOptions...)
+76 -24
View File
@@ -3,6 +3,7 @@ package uixt
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
@@ -779,54 +780,105 @@ func (ad *ADBDriver) GetIme() (ime string, err error) {
return currentIme, nil return currentIme, nil
} }
func (ad *ADBDriver) ScreenRecord(duration time.Duration) (videoPath string, err error) { func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) options := option.NewActionOptions(opts...)
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
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 { if err != nil {
log.Error().Err(err) return "", errors.Wrap(err, "screen record failed")
return "", err }
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 {
return "", errors.Wrap(err, "create screen record file failed")
} }
defer func() { defer func() {
_ = file.Close() _ = file.Close()
}() }()
// scrcpy -s 7d21bb91 --record=file.mp4 -N // start scrcpy
cmd := exec.Command( cmd := exec.Command(
"scrcpy", "scrcpy",
"-s", ad.Device.Serial(), "-s", ad.Device.Serial(),
fmt.Sprintf("--record=%s", fileName), fmt.Sprintf("--record=%s", filePath),
"-N", "--record-format=mp4",
"--max-fps=30",
"--no-playback", // Disable video and audio playback on the computer
) )
cmd.Stdout = io.Discard cmd.Stdout = io.Discard
cmd.Stderr = 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() { go func() {
// 等待 ffmpeg 命令执行完毕
done <- cmd.Wait() done <- cmd.Wait()
}() }()
select { select {
case <-timer: case <-ctx.Done():
// 超时,停止 scrcpy 进程 // 超时,优雅停止 scrcpy 进程
if err := cmd.Process.Signal(syscall.SIGINT); err != nil { 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: case err := <-done:
// ffmpeg 正常结束
if err != nil { if err != nil {
log.Error().Err(err) return "", errors.Wrap(err, "screen record failed")
return "", err
} }
} }
return filepath.Abs(fileName)
return filepath.Abs(filePath)
} }
func (ad *ADBDriver) Setup() error { func (ad *ADBDriver) Setup() error {
+1 -1
View File
@@ -195,7 +195,7 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) {
func TestDriver_ADB_ScreenRecord(t *testing.T) { func TestDriver_ADB_ScreenRecord(t *testing.T) {
driver := setupADBDriverExt(t) driver := setupADBDriverExt(t)
path, err := driver.ScreenRecord(5 * time.Second) path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
assert.Nil(t, err) assert.Nil(t, err)
defer os.Remove(path) defer os.Remove(path)
t.Log(path) t.Log(path)
+1 -1
View File
@@ -614,7 +614,7 @@ func (wd *BrowserDriver) GetSession() *DriverSession {
return nil 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 return
} }
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
_ "image/gif" _ "image/gif"
_ "image/png" _ "image/png"
"time"
"github.com/httprunner/httprunner/v5/uixt/ai" "github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
@@ -37,7 +36,7 @@ type IDriver interface {
ForegroundInfo() (app types.AppInfo, err error) ForegroundInfo() (app types.AppInfo, err error)
WindowSize() (types.Size, error) WindowSize() (types.Size, error)
ScreenShot(opts ...option.ActionOption) (*bytes.Buffer, 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) Source(srcOpt ...option.SourceOption) (string, error)
Orientation() (orientation types.Orientation, err error) Orientation() (orientation types.Orientation, err error)
Rotation() (rotation types.Rotation, err error) Rotation() (rotation types.Rotation, err error)
+1 -1
View File
@@ -273,7 +273,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
case ACTION_ScreenShot: case ACTION_ScreenShot:
// take screenshot // take screenshot
log.Info().Msg("take screenshot for current screen") log.Info().Msg("take screenshot for current screen")
_, err := dExt.GetScreenResult(action.GetScreenOptions()...) _, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
return err return err
case ACTION_ClosePopups: case ACTION_ClosePopups:
return dExt.ClosePopupsHandler() return dExt.ClosePopupsHandler()
+2 -2
View File
@@ -364,11 +364,11 @@ func (s *StubIOSDriver) WindowSize() (types.Size, error) {
return s.WDADriver.WindowSize() 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 { if err := s.SetupWda(); err != nil {
return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error()) return "", errors.Wrap(code.DeviceHTTPDriverError, err.Error())
} }
return s.WDADriver.ScreenRecord(duration) return s.WDADriver.ScreenRecord(opts...)
} }
func (s *StubIOSDriver) Orientation() (types.Orientation, error) { func (s *StubIOSDriver) Orientation() (types.Orientation, error) {
+1 -1
View File
@@ -253,7 +253,7 @@ func (hd *HDCDriver) StopCaptureLog() (result interface{}, err error) {
return hd.points, nil 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 return "", nil
} }
+4 -1
View File
@@ -875,10 +875,13 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
return wd.Session.POST(data, "/gtf/automation/log") 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) 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)) 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) file, err := os.Create(fileName)
if err != nil { if err != nil {
fmt.Println("Error creating file:", err) fmt.Println("Error creating file:", err)
+1 -1
View File
@@ -302,7 +302,7 @@ func TestDriver_WDA_AccessibleSource(t *testing.T) {
func TestDriver_WDA_ScreenRecord(t *testing.T) { func TestDriver_WDA_ScreenRecord(t *testing.T) {
driver := setupWDADriverExt(t) driver := setupWDADriverExt(t)
path, err := driver.ScreenRecord(5 * time.Second) path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5))
assert.Nil(t, err) assert.Nil(t, err)
t.Log(path) t.Log(path)
} }
+3 -4
View File
@@ -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 options
return o.ScreenOptions.Options()
} }
func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) { func (o *ActionOptions) ApplyOffset(absX, absY float64) (float64, float64) {
+45 -1
View File
@@ -4,6 +4,7 @@ import "github.com/httprunner/httprunner/v5/uixt/types"
type ScreenOptions struct { type ScreenOptions struct {
ScreenShotOptions ScreenShotOptions
ScreenRecordOptions
ScreenFilterOptions ScreenFilterOptions
} }
@@ -18,7 +19,7 @@ type ScreenShotOptions struct {
ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"` 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) options := make([]ActionOption, 0)
if o == nil { if o == nil {
return options 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) is the top left corner, (x2, y2) is the bottom right corner
// [x1, y1, x2, y2] in percentage of the screen // [x1, y1, x2, y2] in percentage of the screen
type Scope []float64 type Scope []float64