Skip to content

Commit af9721e

Browse files
Fix iOS video recording for iOS 16 device
1 parent 89b637e commit af9721e

14 files changed

Lines changed: 1480 additions & 48 deletions

File tree

agent/src/main/java/com/microsoft/hydralab/agent/runner/xctest/XCTestRunner.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,37 @@ private void initializeTest(TestRunDevice testRunDevice, TestTask testTask, Test
6969
}
7070

7171
@Override
72-
protected void reInstallApp(TestRunDevice testRunDevice, TestTask testTask, Logger logger) {
72+
protected void reInstallApp(TestRunDevice testRunDevice, TestTask testTask, Logger logger) throws Exception {
7373
checkTestTaskCancel(testTask);
7474
if (testTask.getNeedUninstall()) {
75+
logger.info("📦 Uninstalling app: {}", testTask.getPkgName());
7576
testRunDeviceOrchestrator.uninstallApp(testRunDevice, testTask.getPkgName(), logger);
7677
ThreadUtils.safeSleep(3000);
7778
} else if (testTask.getNeedClearData()) {
79+
logger.info("🧹 Clearing app data: {}", testTask.getPkgName());
7880
testRunDeviceOrchestrator.resetPackage(testRunDevice, testTask.getPkgName(), logger);
7981
}
82+
83+
// Install the app (IPA) if not skipped
84+
if (!testTask.getSkipInstall() && testTask.getAppFile() != null && testTask.getAppFile().exists()) {
85+
logger.info("📲 Installing app from: {}", testTask.getAppFile().getAbsolutePath());
86+
try {
87+
boolean installed = testRunDeviceOrchestrator.installApp(testRunDevice, testTask.getAppFile().getAbsolutePath(), logger);
88+
if (installed) {
89+
logger.info("✅ App installed successfully");
90+
} else {
91+
logger.error("❌ App installation returned false");
92+
throw new Exception("Failed to install app: " + testTask.getAppFile().getAbsolutePath());
93+
}
94+
} catch (Exception e) {
95+
logger.error("❌ App installation failed: {}", e.getMessage());
96+
throw e;
97+
}
98+
} else {
99+
logger.info("⏭️ Skipping app installation (skipInstall={}, appFile={})",
100+
testTask.getSkipInstall(),
101+
testTask.getAppFile() != null ? testTask.getAppFile().getAbsolutePath() : "null");
102+
}
80103
}
81104

82105
private void unzipXctestFolder(File zipFile, TestRun testRun, Logger logger) {

android_client/gradlew

100644100755
File mode changed.

center-application.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
spring:
2+
security:
3+
oauth2:
4+
enabled: false
5+
client:
6+
provider:
7+
azure-ad:
8+
authorization-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
9+
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token
10+
jwk-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
11+
registration:
12+
azure-client:
13+
provider: azure-ad
14+
client-id: dummy-client-id
15+
client-secret: dummy-secret
16+
authorization-grant-type: authorization_code
17+
redirect-uri: http://localhost:9886/login/oauth2/code/azure-client
18+
scope: "User.Read"
19+
datasource:
20+
url: jdbc:sqlite:./hydra_lab_center_db.sqlite
21+
driver-class-name: org.sqlite.JDBC
22+
username: sqlite
23+
password: 98765432
24+
25+
app:
26+
default-user: default@hydralab.com
27+
storage:
28+
type: LOCAL
29+
location: ${user.dir}
30+
agent-auth-mode: SECRET
31+
api-auth-mode: SECRET

common/src/main/java/com/microsoft/hydralab/common/management/AppiumServerManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ public IOSDriver getIOSDriver(DeviceInfo deviceInfo, Logger logger) {
147147
caps.setCapability(IOSMobileCapabilityType.USE_PREBUILT_WDA, false);
148148
caps.setCapability("useXctestrunFile", false);
149149
caps.setCapability("skipLogCapture", true);
150-
caps.setCapability("mjpegServerPort", IOSUtils.getMjpegServerPortByUdid(udid, logger, deviceInfo));
150+
// Note: mjpegServerPort removed - Appium XCUITest driver handles MJPEG streaming internally
151+
// Setting it explicitly was causing port conflicts with iproxy
151152

152153
int tryTimes = 3;
153154
boolean sessionCreated = false;

common/src/main/java/com/microsoft/hydralab/common/management/device/impl/IOSDeviceDriver.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,12 @@ public List<EnvCapabilityRequirement> getEnvCapabilityRequirements() {
8484
}
8585

8686
@Override
87-
public void screenCapture(@NotNull DeviceInfo deviceInfo, @NotNull String path, @Nullable Logger logger) {
88-
IOSUtils.takeScreenshot(deviceInfo.getSerialNum(), path, classLogger);
87+
public void screenCapture(@NotNull DeviceInfo deviceInfo, @NotNull String path, @Nullable Logger logger) throws Exception {
88+
boolean success = IOSUtils.takeScreenshot(deviceInfo.getSerialNum(), path, classLogger);
89+
if (!success) {
90+
throw new Exception("❌ iOS screenshot capture failed for device: " + deviceInfo.getSerialNum() +
91+
". Ensure tunneld is running (for iOS 17+ device only): sudo python3 -m pymobiledevice3 remote tunneld");
92+
}
8993
}
9094

9195
@Override

common/src/main/java/com/microsoft/hydralab/common/screen/IOSAppiumScreenRecorder.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,27 @@ abstract public class IOSAppiumScreenRecorder implements ScreenRecorder {
1919
protected String recordDir;
2020

2121
protected boolean isStarted = false;
22+
protected boolean isDriverInitialized = false;
2223

2324

2425
public IOSAppiumScreenRecorder(DeviceDriver deviceDriver, DeviceInfo info, String recordDir) {
2526
this.deviceDriver = deviceDriver;
2627
this.deviceInfo = info;
2728
this.recordDir = recordDir;
2829

29-
this.iosDriver = deviceDriver.getAppiumServerManager().getIOSDriver(deviceInfo, CLASS_LOGGER);
30+
CLASS_LOGGER.info("🎬 Initializing iOS screen recorder for device: {} ({})", info.getName(), info.getSerialNum());
31+
try {
32+
this.iosDriver = deviceDriver.getAppiumServerManager().getIOSDriver(deviceInfo, CLASS_LOGGER);
33+
if (this.iosDriver != null) {
34+
CLASS_LOGGER.info("✅ IOSDriver initialized successfully for device: {}", info.getSerialNum());
35+
isDriverInitialized = true;
36+
} else {
37+
CLASS_LOGGER.error("❌ Failed to initialize IOSDriver - driver is null. Ensure WDA is installed on device.");
38+
}
39+
} catch (Exception e) {
40+
CLASS_LOGGER.error("❌ Failed to initialize IOSDriver: {}. Video recording will be disabled.", e.getMessage());
41+
CLASS_LOGGER.error("💡 To fix: Ensure WDA (WebDriverAgent) is installed on the device and Appium server is running.");
42+
}
3043
}
3144

3245
@Override

common/src/main/java/com/microsoft/hydralab/common/screen/IOSAppiumScreenRecorderForMac.java

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,58 +13,96 @@
1313
import java.nio.file.Files;
1414
import java.nio.file.Path;
1515
import java.nio.file.Paths;
16-
import java.text.SimpleDateFormat;
1716
import java.time.Duration;
1817
import java.util.Base64;
1918

2019
public class IOSAppiumScreenRecorderForMac extends IOSAppiumScreenRecorder {
2120

2221
public IOSAppiumScreenRecorderForMac(DeviceDriver deviceDriver, DeviceInfo info, String recordDir) {
2322
super(deviceDriver, info, recordDir);
23+
CLASS_LOGGER.info("🎬 IOSAppiumScreenRecorderForMac initialized. Record dir: {}", recordDir);
2424
}
2525

2626
@Override
2727
public void startRecord(int maxTimeInSecond) {
28+
if (!isDriverInitialized || iosDriver == null) {
29+
CLASS_LOGGER.error("❌ Cannot start recording - IOSDriver not initialized. Skipping video recording.");
30+
CLASS_LOGGER.error("💡 Ensure WDA (WebDriverAgent) is installed on the iOS device.");
31+
return;
32+
}
33+
2834
int timeout = maxTimeInSecond > 0 ? maxTimeInSecond : DEFAULT_TIMEOUT_IN_SECOND;
35+
CLASS_LOGGER.info("🎬 Starting iOS screen recording for device: {} (timeout: {}s)",
36+
deviceInfo.getSerialNum(), timeout);
2937
try {
3038
FlowUtil.retryAndSleepWhenFalse(3, 10, () -> {
39+
CLASS_LOGGER.info("📹 Calling iosDriver.startRecordingScreen() with 720p @ 30fps...");
3140
iosDriver.startRecordingScreen(new IOSStartScreenRecordingOptions()
3241
.enableForcedRestart()
33-
.withFps(24)
42+
.withFps(30) // 30 fps for smoother video
3443
.withVideoType("h264")
35-
.withVideoScale("720:360")
44+
.withVideoScale("1280:720") // 720p resolution (was 720:360)
45+
.withVideoQuality(IOSStartScreenRecordingOptions.VideoQuality.HIGH)
3646
.withTimeLimit(Duration.ofSeconds(timeout)));
3747
return true;
3848
});
3949
isStarted = true;
50+
CLASS_LOGGER.info("✅ iOS screen recording started successfully for device: {}", deviceInfo.getSerialNum());
4051
} catch (Exception e) {
41-
System.out.println("-------------------------------Fail to Start recording, Ignore it to unblocking the following tests----------------------------");
42-
e.printStackTrace();
43-
System.out.println("-------------------------------------------------------Ignore End--------------------------------------------------------------");
52+
CLASS_LOGGER.error("❌ Failed to start iOS screen recording: {}", e.getMessage());
53+
CLASS_LOGGER.error("💡 Possible causes: WDA not running, Appium session expired, or device disconnected.");
54+
CLASS_LOGGER.debug("Stack trace:", e);
4455
}
4556
}
4657

4758
@Override
4859
public String finishRecording() {
4960
if (!isStarted) {
61+
CLASS_LOGGER.warn("⚠️ finishRecording() called but recording was never started. Returning null.");
62+
return null;
63+
}
64+
65+
if (iosDriver == null) {
66+
CLASS_LOGGER.error("❌ Cannot stop recording - IOSDriver is null.");
67+
isStarted = false;
5068
return null;
5169
}
52-
SimpleDateFormat format = new SimpleDateFormat(
53-
"yyyy-MM-dd-HH-mm-ss");
70+
71+
CLASS_LOGGER.info("⏹️ Stopping iOS screen recording for device: {}", deviceInfo.getSerialNum());
5472
String destPath = "";
5573
try {
5674
// wait 5s to record more info after testing
75+
CLASS_LOGGER.info("⏳ Waiting 5s before stopping recording...");
5776
ThreadUtils.safeSleep(5000);
77+
78+
CLASS_LOGGER.info("📹 Calling iosDriver.stopRecordingScreen()...");
5879
String base64String = iosDriver.stopRecordingScreen();
80+
81+
if (base64String == null || base64String.isEmpty()) {
82+
CLASS_LOGGER.error("❌ stopRecordingScreen() returned empty data.");
83+
isStarted = false;
84+
return null;
85+
}
86+
5987
byte[] data = Base64.getDecoder().decode(base64String);
6088
destPath = new File(recordDir, Const.ScreenRecoderConfig.DEFAULT_FILE_NAME).getAbsolutePath();
6189
Path path = Paths.get(destPath);
6290
Files.write(path, data);
6391
isStarted = false;
92+
93+
File videoFile = new File(destPath);
94+
if (videoFile.exists() && videoFile.length() > 0) {
95+
CLASS_LOGGER.info("✅ iOS screen recording saved successfully: {} ({}KB)",
96+
destPath, videoFile.length() / 1024);
97+
} else {
98+
CLASS_LOGGER.error("❌ Video file was not created or is empty: {}", destPath);
99+
return null;
100+
}
64101
} catch (Throwable e) {
65-
System.out.println("-------------------------------Fail to Stop recording, Ignore it to unblocking the following tests-----------------------------");
66-
e.printStackTrace();
67-
System.out.println("-------------------------------------------------------Ignore End--------------------------------------------------------------");
102+
CLASS_LOGGER.error("❌ Failed to stop iOS screen recording: {}", e.getMessage());
103+
CLASS_LOGGER.error("💡 Possible causes: Recording timeout exceeded, WDA crashed, or device disconnected.");
104+
CLASS_LOGGER.debug("Stack trace:", e);
105+
isStarted = false;
68106
return null;
69107
}
70108
return destPath;

0 commit comments

Comments
 (0)