Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions commands/screenrecord.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,23 @@ type ScreenRecordRequest struct {
OutputPath string
TimeLimit int // max recording duration in seconds, 0 = no limit
StopChan <-chan struct{} // when non-nil, stops recording when closed (server mode)
Ready chan<- error // optional (server mode): signaled once, with nil once recording is confirmed live or with an error if it failed to start
Silent bool
}

// signalReady notifies req.Ready, if present, that the recording is confirmed
// live (err == nil) or failed to start (err != nil). Safe to call more than
// once or with a nil Ready channel — only the first send has any effect.
func (req ScreenRecordRequest) signalReady(err error) {
if req.Ready == nil {
return
}
select {
case req.Ready <- err:
default:
}
}

// ScreenRecordResponse contains the result of a screen recording
type ScreenRecordResponse struct {
Output string `json:"output"`
Expand All @@ -33,6 +47,7 @@ type ScreenRecordResponse struct {
func ScreenRecordCommand(req ScreenRecordRequest) *CommandResponse {
targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID)
if err != nil {
req.signalReady(err)
return NewErrorResponse(fmt.Errorf("error finding device: %w", err))
}

Expand All @@ -43,6 +58,7 @@ func ScreenRecordCommand(req ScreenRecordRequest) *CommandResponse {
Hook: GetShutdownHook(),
})
if err != nil {
req.signalReady(err)
return NewErrorResponse(fmt.Errorf("error starting agent: %w", err))
}

Expand All @@ -55,6 +71,9 @@ func ScreenRecordCommand(req ScreenRecordRequest) *CommandResponse {
OnDownloadProgress: progress.downloadProgress,
OnDownloaded: progress.downloaded,
}
// no async on-device UI step here (unlike real iOS devices) — the
// recording is live as soon as we're about to dispatch it.
req.signalReady(nil)
return screenRecordNative(func() error {
return dev.ScreenRecord(req.OutputPath, req.TimeLimit, req.StopChan, cb)
}, req, progress)
Expand All @@ -64,23 +83,33 @@ func ScreenRecordCommand(req ScreenRecordRequest) *CommandResponse {
case targetDevice.Platform() == "android":
dev, ok := targetDevice.(*devices.AndroidDevice)
if !ok {
return NewErrorResponse(fmt.Errorf("expected android device"))
err := fmt.Errorf("expected android device")
req.signalReady(err)
return NewErrorResponse(err)
}
req.signalReady(nil)
return screenRecordNative(func() error {
return dev.ScreenRecord(req.OutputPath, req.TimeLimit, req.StopChan)
}, req, progress)
case targetDevice.Platform() == "ios" && targetDevice.DeviceType() == "simulator":
dev, ok := targetDevice.(*devices.SimulatorDevice)
if !ok {
return NewErrorResponse(fmt.Errorf("expected simulator device"))
err := fmt.Errorf("expected simulator device")
req.signalReady(err)
return NewErrorResponse(err)
}
req.signalReady(nil)
return screenRecordNative(func() error {
return dev.ScreenRecord(req.OutputPath, req.TimeLimit, req.StopChan)
}, req, progress)
case targetDevice.Platform() == "ios" && targetDevice.DeviceType() == "real":
// real iOS devices route through DeviceKit + ReplayKit; screenRecordIOSDevice
// signals req.Ready itself once the broadcast picker is confirmed started.
return screenRecordIOSDevice(targetDevice, req, progress)
default:
return NewErrorResponse(fmt.Errorf("screen recording is not supported for this device type"))
err := fmt.Errorf("screen recording is not supported for this device type")
req.signalReady(err)
return NewErrorResponse(err)
}
}

Expand Down Expand Up @@ -165,6 +194,7 @@ func (p *screenRecordProgress) downloaded(speedMBps float64) {
func screenRecordIOSDevice(targetDevice devices.ControllableDevice, req ScreenRecordRequest, progress *screenRecordProgress) *CommandResponse {
tempFile, err := os.CreateTemp("", "screenrecord-*.avc")
if err != nil {
req.signalReady(err)
return NewErrorResponse(fmt.Errorf("error creating temp file: %w", err))
}
tempPath := tempFile.Name()
Expand All @@ -188,6 +218,9 @@ func screenRecordIOSDevice(targetDevice devices.ControllableDevice, req ScreenRe
OnProgress: func(message string) {
utils.Verbose(message)
},
OnReady: func() {
req.signalReady(nil)
},
OnData: withStopChan(func(data []byte) bool {
_, writeErr := tempFile.Write(data)
return writeErr == nil
Expand All @@ -203,6 +236,9 @@ func screenRecordIOSDevice(targetDevice devices.ControllableDevice, req ScreenRe
tempFile.Close()

if err != nil {
// no-op if OnReady already fired above; covers failures that happen
// before DeviceKit/the broadcast picker was ever confirmed live.
req.signalReady(err)
return NewErrorResponse(fmt.Errorf("error during screen capture: %w", err))
}

Expand Down
1 change: 1 addition & 0 deletions devices/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type ScreenCaptureConfig struct {
FPS int
Bitrate int // bitrate in bits per second, only applies to AVC (0 for default)
OnProgress func(message string) // optional progress callback
OnReady func() // optional: called once capture is confirmed live (e.g. after the ReplayKit broadcast picker is clicked), before streaming begins
OnData func([]byte) bool // data callback - return false to stop
}

Expand Down
17 changes: 17 additions & 0 deletions devices/devicekit/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package devicekit

import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -29,6 +31,21 @@ func NewDeviceKitClient(hostPort string) *DeviceKitClient {
}
}

// Port returns the port this client talks to, parsed from its base URL.
func (c *DeviceKitClient) Port() int {
parsed, err := url.Parse(c.baseURL)
if err != nil {
return 0
}

port, err := strconv.Atoi(parsed.Port())
if err != nil {
return 0
}

return port
}

type TapAction struct {
Type string `json:"type"`
Duration int `json:"duration"`
Expand Down
Loading
Loading