@@ -902,15 +902,57 @@ func (d *IOSDevice) StartScreenCapture(config ScreenCaptureConfig) error {
902902 // handle avc format via DeviceKit
903903 if config .Format == "avc" {
904904 if config .OnProgress != nil {
905- config .OnProgress ("Starting DeviceKit for H.264 streaming " )
905+ config .OnProgress ("Checking DeviceKit status " )
906906 }
907907
908- // start DeviceKit
909- // Note: passing nil registry since this is internal call from StartScreenCapture
910- // ScreenCapture callers should have already registered the device via StartAgent
911- deviceKitInfo , err := d .StartDeviceKit (nil )
912- if err != nil {
913- return fmt .Errorf ("failed to start DeviceKit: %w" , err )
908+ var deviceKitInfo * DeviceKitInfo
909+ var err error
910+
911+ // check if DeviceKit is already running
912+ if d .isDeviceKitRunning () {
913+ utils .Verbose ("DeviceKit already running, reusing existing session" )
914+
915+ // check if we need to create port forwarders
916+ d .mu .Lock ()
917+ hasHTTPForwarder := d .portForwarderDeviceKit != nil && d .portForwarderDeviceKit .IsRunning ()
918+ hasStreamForwarder := d .portForwarderAvc != nil && d .portForwarderAvc .IsRunning ()
919+ d .mu .Unlock ()
920+
921+ if hasHTTPForwarder && hasStreamForwarder {
922+ // reuse existing forwarders
923+ d .mu .Lock ()
924+ httpPort , _ := d .portForwarderDeviceKit .GetPorts ()
925+ streamPort , _ := d .portForwarderAvc .GetPorts ()
926+ d .mu .Unlock ()
927+
928+ deviceKitInfo = & DeviceKitInfo {
929+ HTTPPort : httpPort ,
930+ StreamPort : streamPort ,
931+ }
932+ } else {
933+ // DeviceKit running but we need to create forwarders
934+ deviceKitInfo , err = d .ensureDeviceKitPortForwarders ()
935+ if err != nil {
936+ return fmt .Errorf ("failed to create port forwarders: %w" , err )
937+ }
938+ }
939+
940+ if config .OnProgress != nil {
941+ config .OnProgress ("Using existing DeviceKit session" )
942+ }
943+ } else {
944+ // DeviceKit not running, start it normally
945+ if config .OnProgress != nil {
946+ config .OnProgress ("Starting DeviceKit for H.264 streaming" )
947+ }
948+
949+ // start DeviceKit
950+ // Note: passing nil registry since this is internal call from StartScreenCapture
951+ // ScreenCapture callers should have already registered the device via StartAgent
952+ deviceKitInfo , err = d .StartDeviceKit (nil )
953+ if err != nil {
954+ return fmt .Errorf ("failed to start DeviceKit: %w" , err )
955+ }
914956 }
915957
916958 if config .OnProgress != nil {
@@ -1228,6 +1270,134 @@ func filterButtons(elements []ScreenElement) []ScreenElement {
12281270 return buttons
12291271}
12301272
1273+ func (d * IOSDevice ) ensureDeviceKitPortForwarders () (* DeviceKitInfo , error ) {
1274+ var httpPort , streamPort int
1275+ var err error
1276+
1277+ // check if HTTP forwarder exists, create if needed
1278+ d .mu .Lock ()
1279+ hasHTTPForwarder := d .portForwarderDeviceKit != nil && d .portForwarderDeviceKit .IsRunning ()
1280+ d .mu .Unlock ()
1281+
1282+ if ! hasHTTPForwarder {
1283+ httpPort , err = findAvailablePortInRange (portRangeStart , portRangeEnd )
1284+ if err != nil {
1285+ return nil , fmt .Errorf ("failed to find available port for HTTP: %w" , err )
1286+ }
1287+
1288+ forwarder := ios .NewPortForwarder (d .ID ())
1289+ err = forwarder .Forward (httpPort , deviceKitHTTPPort )
1290+ if err != nil {
1291+ return nil , fmt .Errorf ("failed to forward HTTP port: %w" , err )
1292+ }
1293+
1294+ d .mu .Lock ()
1295+ d .portForwarderDeviceKit = forwarder
1296+ d .mu .Unlock ()
1297+ utils .Verbose ("Port forwarding created: localhost:%d -> device:%d (HTTP)" , httpPort , deviceKitHTTPPort )
1298+ } else {
1299+ d .mu .Lock ()
1300+ httpPort , _ = d .portForwarderDeviceKit .GetPorts ()
1301+ d .mu .Unlock ()
1302+ }
1303+
1304+ // check if stream forwarder exists, create if needed
1305+ d .mu .Lock ()
1306+ hasStreamForwarder := d .portForwarderAvc != nil && d .portForwarderAvc .IsRunning ()
1307+ d .mu .Unlock ()
1308+
1309+ if ! hasStreamForwarder {
1310+ streamPort , err = findAvailablePortInRange (portRangeStart , portRangeEnd )
1311+ if err != nil {
1312+ if ! hasHTTPForwarder {
1313+ _ = d .portForwarderDeviceKit .Stop ()
1314+ }
1315+ return nil , fmt .Errorf ("failed to find available port for stream: %w" , err )
1316+ }
1317+
1318+ d .mu .Lock ()
1319+ d .portForwarderAvc = ios .NewPortForwarder (d .ID ())
1320+ d .mu .Unlock ()
1321+
1322+ err = d .portForwarderAvc .Forward (streamPort , deviceKitStreamPort )
1323+ if err != nil {
1324+ if ! hasHTTPForwarder {
1325+ _ = d .portForwarderDeviceKit .Stop ()
1326+ }
1327+ return nil , fmt .Errorf ("failed to forward stream port: %w" , err )
1328+ }
1329+ utils .Verbose ("Port forwarding created: localhost:%d -> device:%d (H.264 stream)" , streamPort , deviceKitStreamPort )
1330+ } else {
1331+ d .mu .Lock ()
1332+ streamPort , _ = d .portForwarderAvc .GetPorts ()
1333+ d .mu .Unlock ()
1334+ }
1335+
1336+ return & DeviceKitInfo {
1337+ HTTPPort : httpPort ,
1338+ StreamPort : streamPort ,
1339+ }, nil
1340+ }
1341+
1342+ func (d * IOSDevice ) isDeviceKitRunning () bool {
1343+ // check if we already have port forwarders running
1344+ d .mu .Lock ()
1345+ hasHTTPForwarder := d .portForwarderDeviceKit != nil && d .portForwarderDeviceKit .IsRunning ()
1346+ hasStreamForwarder := d .portForwarderAvc != nil && d .portForwarderAvc .IsRunning ()
1347+ d .mu .Unlock ()
1348+
1349+ // if both forwarders exist, DeviceKit is definitely running from our perspective
1350+ if hasHTTPForwarder && hasStreamForwarder {
1351+ utils .Verbose ("DeviceKit port forwarders already running" )
1352+ return true
1353+ }
1354+
1355+ // find an available local port for testing
1356+ testPort , err := findAvailablePortInRange (portRangeStart , portRangeEnd )
1357+ if err != nil {
1358+ utils .Verbose ("Could not find available port for DeviceKit check: %v" , err )
1359+ return false
1360+ }
1361+
1362+ // create temporary port forwarder to device port 12005 (stream)
1363+ testForwarder := ios .NewPortForwarder (d .ID ())
1364+ err = testForwarder .Forward (testPort , deviceKitStreamPort )
1365+ if err != nil {
1366+ utils .Verbose ("Could not create test port forwarder: %v" , err )
1367+ return false
1368+ }
1369+
1370+ // ensure cleanup of test forwarder
1371+ defer func () {
1372+ _ = testForwarder .Stop ()
1373+ }()
1374+
1375+ // try to connect with timeout
1376+ conn , err := net .DialTimeout ("tcp" , fmt .Sprintf ("localhost:%d" , testPort ), 2 * time .Second )
1377+ if err != nil {
1378+ utils .Verbose ("DeviceKit not responding on port %d: %v" , deviceKitStreamPort , err )
1379+ return false
1380+ }
1381+ defer func () { _ = conn .Close () }()
1382+
1383+ // set read deadline and try to read 1 byte
1384+ err = conn .SetReadDeadline (time .Now ().Add (1 * time .Second ))
1385+ if err != nil {
1386+ utils .Verbose ("Could not set read deadline: %v" , err )
1387+ return false
1388+ }
1389+
1390+ buffer := make ([]byte , 1 )
1391+ _ , err = conn .Read (buffer )
1392+ if err != nil {
1393+ utils .Verbose ("DeviceKit not serving data on port %d: %v" , deviceKitStreamPort , err )
1394+ return false
1395+ }
1396+
1397+ utils .Verbose ("DeviceKit is already running on device port %d" , deviceKitStreamPort )
1398+ return true
1399+ }
1400+
12311401// StartDeviceKit starts the devicekit-ios XCUITest which provides:
12321402// - An HTTP server for tap/dumpUI commands (port 12004)
12331403// - A broadcast extension for H.264 screen streaming (port 12005)
@@ -1312,16 +1482,23 @@ func (d *IOSDevice) StartDeviceKit(hook *ShutdownHook) (*DeviceKitInfo, error) {
13121482 return nil , fmt .Errorf ("failed to launch DeviceKit app: %w" , err )
13131483 }
13141484
1315- // Wait for the app to launch and show the broadcast picker
1316- utils .Verbose ("Waiting %v for DeviceKit app to launch..." , deviceKitAppLaunchTimeout )
1317- time .Sleep (deviceKitAppLaunchTimeout )
1485+ // wait for the app to be in foreground
1486+ utils .Verbose ("Waiting for DeviceKit app to be in foreground..." )
1487+ err = d .waitForAppInForeground (devicekitMainAppBundleId , deviceKitAppLaunchTimeout )
1488+ if err != nil {
1489+ // clean up port forwarders on failure
1490+ _ = d .portForwarderDeviceKit .Stop ()
1491+ _ = d .portForwarderAvc .Stop ()
1492+ return nil , fmt .Errorf ("failed to wait for DeviceKit app: %w" , err )
1493+ }
13181494
13191495 // Start WebDriverAgent to be able to tap on the screen
13201496 err = d .StartAgent (StartAgentConfig {
13211497 OnProgress : func (message string ) {
13221498 utils .Verbose (message )
13231499 },
13241500 })
1501+
13251502 if err != nil {
13261503 // clean up port forwarders on failure
13271504 _ = d .portForwarderDeviceKit .Stop ()
@@ -1354,6 +1531,31 @@ func (d *IOSDevice) StartDeviceKit(hook *ShutdownHook) (*DeviceKitInfo, error) {
13541531 }, nil
13551532}
13561533
1534+ // waitForAppInForeground polls WDA to check if the specified app is in foreground
1535+ func (d * IOSDevice ) waitForAppInForeground (bundleID string , timeout time.Duration ) error {
1536+ deadline := time .After (timeout )
1537+ ticker := time .NewTicker (200 * time .Millisecond )
1538+ defer ticker .Stop ()
1539+
1540+ for {
1541+ select {
1542+ case <- deadline :
1543+ return fmt .Errorf ("timeout waiting for app %s to be in foreground" , bundleID )
1544+ case <- ticker .C :
1545+ activeApp , err := d .wdaClient .GetActiveAppInfo ()
1546+ if err != nil {
1547+ // continue trying on error
1548+ continue
1549+ }
1550+
1551+ if activeApp .BundleID == bundleID {
1552+ utils .Verbose ("App %s is now in foreground" , bundleID )
1553+ return nil
1554+ }
1555+ }
1556+ }
1557+ }
1558+
13571559// findAvailablePortInRange finds an available port in the specified range
13581560func findAvailablePortInRange (start , end int ) (int , error ) {
13591561 for port := start ; port <= end ; port ++ {
0 commit comments