From 5b503a4394383d29d764631a4d7675214a30c1cc Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Thu, 6 Mar 2025 13:28:04 +0800 Subject: [PATCH 1/5] feat: get androidsystemVersion with gadb --- internal/version/VERSION | 2 +- pkg/gadb/device.go | 17 +++++++++++++++-- pkg/gadb/device_test.go | 25 ++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 4c2cfbf6..a2ecc7dd 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503052233 +v5.0.0-beta-2503061328 diff --git a/pkg/gadb/device.go b/pkg/gadb/device.go index 9a9abd4d..63407da9 100644 --- a/pkg/gadb/device.go +++ b/pkg/gadb/device.go @@ -147,15 +147,28 @@ func (d *Device) Usb() (string, error) { return "", errors.New("does not have attribute: usb") } +func (d *Device) SystemVersion() (string, error) { + if d.HasAttribute("systemVersion") { + return d.attrs["systemVersion"], nil + } + systemVersion, err := d.RunShellCommand("getprop", "ro.build.version.release") + systemVersion = strings.TrimSpace(systemVersion) + if err != nil { + return "", errors.New("get android system version failed") + } + d.attrs["systemVersion"] = systemVersion + return systemVersion, nil +} + func (d *Device) SdkVersion() (string, error) { if d.HasAttribute("sdkVersion") { return d.attrs["sdkVersion"], nil } sdkVersion, err := d.RunShellCommand("getprop", "ro.build.version.sdk") - sdkVersion = strings.TrimSpace(sdkVersion) if err != nil { - return "", errors.New("does not have attribute: sdkVersion") + return "", errors.New("get android sdk version failed") } + sdkVersion = strings.TrimSpace(sdkVersion) d.attrs["sdkVersion"] = sdkVersion return sdkVersion, nil } diff --git a/pkg/gadb/device_test.go b/pkg/gadb/device_test.go index 99df395d..81e2e2bc 100644 --- a/pkg/gadb/device_test.go +++ b/pkg/gadb/device_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var devices []*Device @@ -17,9 +20,7 @@ func setupDevices(t *testing.T) { var err error setupClient(t) devices, err = adbClient.DeviceList() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) } func TestDevice_State(t *testing.T) { @@ -119,6 +120,24 @@ func TestDevice_DeviceInfo(t *testing.T) { } } +func TestDevice_SdkVersion(t *testing.T) { + setupDevices(t) + for _, device := range devices { + sdkVersion, err := device.SdkVersion() + assert.Nil(t, err) + t.Log(device.Serial(), sdkVersion) + } +} + +func TestDevice_SystemVersion(t *testing.T) { + setupDevices(t) + for _, device := range devices { + systemVersion, err := device.SystemVersion() + assert.Nil(t, err) + t.Log(device.Serial(), systemVersion) + } +} + func TestDevice_Forward(t *testing.T) { setupDevices(t) From cc81c00a8205c71a5953f43fe3e0e8639853a798 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Thu, 6 Mar 2025 16:57:51 +0800 Subject: [PATCH 2/5] feat: add adb screen record --- internal/version/VERSION | 2 +- pkg/gadb/device.go | 23 ++++++- server/source.go | 2 +- uixt/android_driver_adb.go | 100 ++++++++++++++++++++++------- uixt/android_test.go | 2 +- uixt/browser_driver.go | 2 +- uixt/driver.go | 3 +- uixt/driver_action.go | 2 +- uixt/driver_ext/ios_stub_driver.go | 4 +- uixt/harmony_driver_hdc.go | 2 +- uixt/ios_driver_wda.go | 5 +- uixt/ios_test.go | 2 +- uixt/option/action.go | 7 +- uixt/option/screen.go | 46 ++++++++++++- 14 files changed, 159 insertions(+), 43 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index a2ecc7dd..5f22f65d 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503061328 +v5.0.0-beta-2503061657 diff --git a/pkg/gadb/device.go b/pkg/gadb/device.go index 63407da9..fcc81c3e 100644 --- a/pkg/gadb/device.go +++ b/pkg/gadb/device.go @@ -745,7 +745,7 @@ func (d *Device) ScreenCap() ([]byte, error) { time.Now().Unix()) _, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath) if err != nil { - return nil, err + return nil, errors.Wrap(err, "screencap failed") } // remove temp file @@ -755,5 +755,24 @@ func (d *Device) ScreenCap() ([]byte, error) { buffer := bytes.NewBuffer(nil) 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 } diff --git a/server/source.go b/server/source.go index 87cbec7a..aac7e204 100644 --- a/server/source.go +++ b/server/source.go @@ -37,7 +37,7 @@ func (r *Router) screenResultHandler(c *gin.Context) { var actionOptions []option.ActionOption if screenReq.Options != nil { - actionOptions = screenReq.Options.Options() + actionOptions = screenReq.Options.GetScreenShotOptions() } screenResult, err := driver.GetScreenResult(actionOptions...) diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 1026ce7c..f13050ef 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -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 { diff --git a/uixt/android_test.go b/uixt/android_test.go index f492740c..a67166c0 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -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) diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 95d08123..7a455529 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -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 } diff --git a/uixt/driver.go b/uixt/driver.go index a27a9a48..70accc51 100644 --- a/uixt/driver.go +++ b/uixt/driver.go @@ -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) diff --git a/uixt/driver_action.go b/uixt/driver_action.go index e20e261c..42a8ff66 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -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() diff --git a/uixt/driver_ext/ios_stub_driver.go b/uixt/driver_ext/ios_stub_driver.go index 948ff9be..6dd7a980 100644 --- a/uixt/driver_ext/ios_stub_driver.go +++ b/uixt/driver_ext/ios_stub_driver.go @@ -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) { diff --git a/uixt/harmony_driver_hdc.go b/uixt/harmony_driver_hdc.go index 2c0f0698..fb248791 100644 --- a/uixt/harmony_driver_hdc.go +++ b/uixt/harmony_driver_hdc.go @@ -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 } diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index 5d6f6381..8a97a19b 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -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) diff --git a/uixt/ios_test.go b/uixt/ios_test.go index 98a60a57..6fe37bfd 100644 --- a/uixt/ios_test.go +++ b/uixt/ios_test.go @@ -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) } diff --git a/uixt/option/action.go b/uixt/option/action.go index 4447638a..9692ba6b 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -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) { diff --git a/uixt/option/screen.go b/uixt/option/screen.go index 60af4b31..d516b93a 100644 --- a/uixt/option/screen.go +++ b/uixt/option/screen.go @@ -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 From 79e0323471547a4b88c033b02db3e2383a117f04 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Thu, 6 Mar 2025 17:50:01 +0800 Subject: [PATCH 3/5] fix: screen record with scrcpy --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 21 +++++++++++++++++---- uixt/android_test.go | 22 +++++++++++++++++++--- uixt/option/screen.go | 16 +++++++++++++--- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 5f22f65d..88754c49 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503061657 +v5.0.0-beta-2503061750 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index f13050ef..51668140 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -791,7 +791,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) } - duration := options.Duration + duration := options.ScreenRecordDuration audioOn := options.ScreenRecordWithAudio // get android system version @@ -806,7 +806,9 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string } var useAdbScreenRecord bool - if !audioOn { + if options.ScreenRecordWithScrcpy { + useAdbScreenRecord = false + } else if !audioOn { log.Info().Bool("audioOn", audioOn).Msg("screen record with adb screenrecord by default") useAdbScreenRecord = true } else if sysVersion != 0 && sysVersion < 11 { @@ -817,6 +819,17 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string useAdbScreenRecord = true } + defer func() { + if err == nil { + filePath, err = filepath.Abs(filePath) + if err != nil { + err = errors.Wrap(err, "get absolute path failed") + } else { + log.Info().Str("path", filePath).Msg("screen record success") + } + } + }() + if useAdbScreenRecord { res, err := ad.Device.ScreenRecord(duration) if err != nil { @@ -829,7 +842,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string } // screen record with audio - log.Info().Msg("screen record with audio, use scrcpy") + log.Info().Float64("duration(s)", duration).Msg("screen record with audio, use scrcpy") file, err := os.Create(filePath) if err != nil { @@ -878,7 +891,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string } } - return filepath.Abs(filePath) + return filePath, nil } func (ad *ADBDriver) Setup() error { diff --git a/uixt/android_test.go b/uixt/android_test.go index a67166c0..df78b307 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -195,10 +195,26 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) { func TestDriver_ADB_ScreenRecord(t *testing.T) { driver := setupADBDriverExt(t) - path, err := driver.ScreenRecord(option.WithScreenRecordDuation(5)) + path1, err := driver.ScreenRecord(option.WithScreenRecordDuation(182)) assert.Nil(t, err) - defer os.Remove(path) - t.Log(path) + defer os.Remove(path1) + t.Log(path1) + + path2, err := driver.ScreenRecord( + option.WithScreenRecordDuation(5), + option.WithScreenRecordAudio(true), + ) + assert.Nil(t, err) + defer os.Remove(path2) + t.Log(path2) + + path3, err := driver.ScreenRecord( + option.WithScreenRecordDuation(5), + option.WithScreenRecordScrcpy(true), + ) + assert.Nil(t, err) + defer os.Remove(path3) + t.Log(path3) } func TestDriver_ADB_Backspace(t *testing.T) { diff --git a/uixt/option/screen.go b/uixt/option/screen.go index d516b93a..71665e92 100644 --- a/uixt/option/screen.go +++ b/uixt/option/screen.go @@ -127,9 +127,10 @@ 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"` + ScreenRecordDuration float64 `json:"screenrecord_duration,omitempty" yaml:"screenrecord_duration,omitempty"` + ScreenRecordWithAudio bool `json:"screenrecord_with_audio,omitempty" yaml:"screenrecord_with_audio,omitempty"` + ScreenRecordWithScrcpy bool `json:"screenrecord_with_scrcpy,omitempty" yaml:"screenrecord_with_scrcpy,omitempty"` + ScreenRecordPath string `json:"screenrecord_path,omitempty" yaml:"screenrecord_path,omitempty"` } func (o *ScreenRecordOptions) GetScreenRecordOptions() []ActionOption { @@ -145,6 +146,9 @@ func (o *ScreenRecordOptions) GetScreenRecordOptions() []ActionOption { if o.ScreenRecordWithAudio { options = append(options, WithScreenRecordAudio(true)) } + if o.ScreenRecordWithScrcpy { + options = append(options, WithScreenRecordScrcpy(true)) + } if o.ScreenRecordPath != "" { options = append(options, WithScreenRecordPath(o.ScreenRecordPath)) } @@ -163,6 +167,12 @@ func WithScreenRecordAudio(audioOn bool) ActionOption { } } +func WithScreenRecordScrcpy(scrcpyOn bool) ActionOption { + return func(o *ActionOptions) { + o.ScreenRecordWithScrcpy = scrcpyOn + } +} + func WithScreenRecordPath(path string) ActionOption { return func(o *ActionOptions) { o.ScreenRecordPath = path From e8025f9e654288eb025adc8d5005054aab0f7712 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Thu, 6 Mar 2025 17:58:16 +0800 Subject: [PATCH 4/5] fix: screen record duration --- internal/version/VERSION | 2 +- uixt/android_driver_adb.go | 3 +++ uixt/android_test.go | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 88754c49..47e2af34 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503061750 +v5.0.0-beta-2503061758 diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index 51668140..ae4634ce 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -792,6 +792,9 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string } duration := options.ScreenRecordDuration + if duration == 0 { + duration = options.Duration + } audioOn := options.ScreenRecordWithAudio // get android system version diff --git a/uixt/android_test.go b/uixt/android_test.go index df78b307..8c098b71 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -195,7 +195,8 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) { func TestDriver_ADB_ScreenRecord(t *testing.T) { driver := setupADBDriverExt(t) - path1, err := driver.ScreenRecord(option.WithScreenRecordDuation(182)) + path1, err := driver.ScreenRecord( + option.WithScreenRecordDuation(5)) assert.Nil(t, err) defer os.Remove(path1) t.Log(path1) From 0d416e74a1476d179e7a84c4714c1516bfe2cec0 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Thu, 6 Mar 2025 22:16:49 +0800 Subject: [PATCH 5/5] change: use context to contral ScreenRecord timeout or cancel --- internal/version/VERSION | 2 +- pkg/gadb/device.go | 35 ++++++++++++++++++++++++------ pkg/gadb/device_test.go | 31 +++++++++++++++++++++++++++ uixt/android_driver_adb.go | 44 ++++++++++++++++++++++---------------- uixt/android_test.go | 26 ++++++++++++++++++++++ uixt/option/action.go | 11 ++++++++++ uixt/option/screen.go | 4 +++- 7 files changed, 126 insertions(+), 27 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 47e2af34..a74704b2 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503061758 +v5.0.0-beta-2503062216 diff --git a/pkg/gadb/device.go b/pkg/gadb/device.go index fcc81c3e..75bb376a 100644 --- a/pkg/gadb/device.go +++ b/pkg/gadb/device.go @@ -2,6 +2,7 @@ package gadb import ( "bytes" + "context" "encoding/binary" "fmt" "io" @@ -761,16 +762,38 @@ func (d *Device) ScreenCap() ([]byte, error) { return buffer.Bytes(), nil } -func (d *Device) ScreenRecord(seconds float64) ([]byte, error) { +func (d *Device) ScreenRecord(ctx context.Context) ([]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") + + done := make(chan error, 1) + go func() { + _, err := d.RunShellCommandWithBytes("screenrecord", videoPath) + done <- err + }() + + select { + case <-ctx.Done(): + // timeout or cancelled + pid, err := d.RunShellCommand("pidof", "screenrecord") + if err == nil && pid != "" { + // 发送 SIGINT 信号终止录屏 + _, _ = d.RunShellCommand("kill", "-2", strings.TrimSpace(pid)) + } + <-done // 等待进程完全退出 + case err := <-done: + // adb screenrecord will exit on reached 180s + if err != nil { + return nil, errors.Wrap(err, "screenrecord failed") + } } + // remove temp file + defer func() { + go d.RunShellCommand("rm", videoPath) + }() + buffer := bytes.NewBuffer(nil) - err = d.Pull(videoPath, buffer) + err := d.Pull(videoPath, buffer) if err != nil { return nil, errors.Wrap(err, "pull video failed") } diff --git a/pkg/gadb/device_test.go b/pkg/gadb/device_test.go index 81e2e2bc..0a55e56b 100644 --- a/pkg/gadb/device_test.go +++ b/pkg/gadb/device_test.go @@ -4,6 +4,7 @@ package gadb import ( "bytes" + "context" "os" "reflect" "strings" @@ -295,6 +296,36 @@ func TestDevice_Pull(t *testing.T) { } } +func TestDevice_ScreenRecord(t *testing.T) { + setupDevices(t) + + for _, dev := range devices { + // screen record with time limit 5 seconds + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + if _, err := dev.ScreenRecord(ctx); err != nil { + assert.Nil(t, err) + } + cancel() + } + + for _, dev := range devices { + // screen record with cancel signal + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error) + go func() { + _, err := dev.ScreenRecord(ctx) + done <- err + }() + + // record for 3 seconds + time.Sleep(time.Second * 3) + cancel() + + err := <-done + assert.Nil(t, err) + } +} + func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) { type fields struct { adbClient Client diff --git a/uixt/android_driver_adb.go b/uixt/android_driver_adb.go index ae4634ce..22f63b54 100644 --- a/uixt/android_driver_adb.go +++ b/uixt/android_driver_adb.go @@ -791,11 +791,25 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) } + var ctx context.Context + if options.Context != nil { + ctx = options.Context + } else { + ctx = context.Background() + } + + var cancel context.CancelFunc duration := options.ScreenRecordDuration if duration == 0 { duration = options.Duration } - audioOn := options.ScreenRecordWithAudio + if duration != 0 { + ctx, cancel = context.WithTimeout(ctx, + time.Duration(duration*float64(time.Second))) + } else { + ctx, cancel = context.WithCancel(ctx) + } + defer cancel() // get android system version var sysVersion int @@ -809,6 +823,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string } var useAdbScreenRecord bool + audioOn := options.ScreenRecordWithAudio if options.ScreenRecordWithScrcpy { useAdbScreenRecord = false } else if !audioOn { @@ -834,7 +849,9 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string }() if useAdbScreenRecord { - res, err := ad.Device.ScreenRecord(duration) + // screen record with adb screenrecord + // adb screenrecord duration is limited in range [1,180] seconds + res, err := ad.Device.ScreenRecord(ctx) if err != nil { return "", errors.Wrap(err, "screen record failed") } @@ -844,16 +861,8 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string return filePath, nil } - // screen record with audio - log.Info().Float64("duration(s)", duration).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() { - _ = file.Close() - }() + // screen record with scrcpy + log.Info().Float64("duration(s)", duration).Msg("screen record with scrcpy") // start scrcpy cmd := exec.Command( @@ -866,15 +875,10 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string ) cmd.Stdout = io.Discard cmd.Stderr = io.Discard - 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() { done <- cmd.Wait() @@ -882,15 +886,17 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string select { case <-ctx.Done(): - // 超时,优雅停止 scrcpy 进程 + // timeout or cancelled + log.Info().Msg("screen recording stopped") if err := cmd.Process.Signal(syscall.SIGINT); err != nil { log.Error().Err(err).Msg("failed to stop scrcpy process") _ = cmd.Process.Kill() // 强制结束进程 } <-done // 等待进程完全退出 case err := <-done: + log.Info().Msg("scrcpy exited") if err != nil { - return "", errors.Wrap(err, "screen record failed") + return "", errors.Wrap(err, "screen record with scrcpy failed") } } diff --git a/uixt/android_test.go b/uixt/android_test.go index 8c098b71..c18c67e0 100644 --- a/uixt/android_test.go +++ b/uixt/android_test.go @@ -3,6 +3,7 @@ package uixt import ( + "context" "os" "testing" "time" @@ -195,12 +196,15 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) { func TestDriver_ADB_ScreenRecord(t *testing.T) { driver := setupADBDriverExt(t) + + // adb screenrecord --time-limit 5 path1, err := driver.ScreenRecord( option.WithScreenRecordDuation(5)) assert.Nil(t, err) defer os.Remove(path1) t.Log(path1) + // scrcpy with time limit path2, err := driver.ScreenRecord( option.WithScreenRecordDuation(5), option.WithScreenRecordAudio(true), @@ -209,6 +213,7 @@ func TestDriver_ADB_ScreenRecord(t *testing.T) { defer os.Remove(path2) t.Log(path2) + // scrcpy with time limit path3, err := driver.ScreenRecord( option.WithScreenRecordDuation(5), option.WithScreenRecordScrcpy(true), @@ -216,6 +221,27 @@ func TestDriver_ADB_ScreenRecord(t *testing.T) { assert.Nil(t, err) defer os.Remove(path3) t.Log(path3) + + // scrcpy with cancel signal + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error) + go func() { + path4, err := driver.ScreenRecord( + option.WithContext(ctx), + option.WithScreenRecordScrcpy(true), + ) + assert.Nil(t, err) + defer os.Remove(path4) + t.Log(path4) + done <- err + }() + + // record for 3 seconds + time.Sleep(time.Second * 3) + cancel() + + err = <-done + assert.Nil(t, err) } func TestDriver_ADB_Backspace(t *testing.T) { diff --git a/uixt/option/action.go b/uixt/option/action.go index 9692ba6b..594a6cd4 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -1,12 +1,14 @@ package option import ( + "context" "math/rand/v2" "github.com/httprunner/httprunner/v5/internal/builtin" ) type ActionOptions struct { + Context context.Context // log Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log @@ -33,6 +35,9 @@ func (o *ActionOptions) Options() []ActionOption { return options } + if o.Context != nil { + options = append(options, WithContext(o.Context)) + } if o.Identifier != "" { options = append(options, WithIdentifier(o.Identifier)) } @@ -206,6 +211,12 @@ func NewActionOptions(opts ...ActionOption) *ActionOptions { type ActionOption func(o *ActionOptions) +func WithContext(ctx context.Context) ActionOption { + return func(o *ActionOptions) { + o.Context = ctx + } +} + func WithCustomOption(key string, value interface{}) ActionOption { return func(o *ActionOptions) { if o.Custom == nil { diff --git a/uixt/option/screen.go b/uixt/option/screen.go index 71665e92..160dd4c9 100644 --- a/uixt/option/screen.go +++ b/uixt/option/screen.go @@ -1,6 +1,8 @@ package option -import "github.com/httprunner/httprunner/v5/uixt/types" +import ( + "github.com/httprunner/httprunner/v5/uixt/types" +) type ScreenOptions struct { ScreenShotOptions