@@ -45,6 +45,7 @@ final class DeviceHarnessViewModel: ObservableObject {
4545 private var observers : [ NSObjectProtocol ] = [ ]
4646 private var didRunAutomation = false
4747 private var allowDirectICE = false
48+ private var foregroundRecoveryEnabled = false
4849 private var subscriberToken = " "
4950 private var subscriberConnectTask : Task < Void , Never > ?
5051 private var observedLifecycleEvents : Set < String > = [ ]
@@ -60,6 +61,7 @@ final class DeviceHarnessViewModel: ObservableObject {
6061 private var foregroundSoakReconnectAttemptCount = 0
6162 private var isSoakReconnectInProgress = false
6263 private var soakReconnectLeaseExpiresAt : Date ?
64+ private let appLaunchDate = Date ( )
6365 private static let voiceIsolationRecommendationKey =
6466 " LiveKitNativeDeviceHarness.didSuggestAppleVoiceIsolation "
6567
@@ -70,6 +72,8 @@ final class DeviceHarnessViewModel: ObservableObject {
7072 execution_environment: \( Self . executionEnvironment)
7173 device_model: \( UIDevice . current. model)
7274 system_version: \( UIDevice . current. systemName) \( UIDevice . current. systemVersion)
75+ app_uptime_seconds: \( Self . formatSeconds ( appUptimeSeconds) )
76+ process_cpu_time_seconds: \( Self . formatSeconds ( DeviceHarnessProcessSampler . cpuTimeSeconds ( ) ) )
7377 """
7478 status = " App launch observed. "
7579 diagnosticsText = appInstallReport
@@ -129,7 +133,7 @@ final class DeviceHarnessViewModel: ObservableObject {
129133 try await publishInitialMedia (
130134 room: room,
131135 audioOptions: AudioCaptureOptions ( echoCancellation: true ) ,
132- cameraOptions: CameraCaptureOptions ( width: 1280 , height: 720 , framesPerSecond: 30 ) ,
136+ cameraOptions: CameraCaptureOptions ( width: 720 , height: 1280 , framesPerSecond: 30 ) ,
133137 cameraSimulcast: true
134138 )
135139 }
@@ -211,6 +215,7 @@ final class DeviceHarnessViewModel: ObservableObject {
211215 subscriberToken = environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_SUBSCRIBER_TOKEN " ] ?? subscriberToken
212216 batterySaverWebRTC = environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_BATTERY_SAVER_WEBRTC " ] == " 1 "
213217 allowDirectICE = environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_ALLOW_DIRECT_ICE " ] == " 1 "
218+ foregroundRecoveryEnabled = environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_FOREGROUND_RECOVERY " ] == " 1 "
214219 observedLifecycleEvents. removeAll ( )
215220 emitHarnessLog ( " scripted_validation: autostart requested " )
216221 if batterySaverWebRTC {
@@ -242,7 +247,7 @@ final class DeviceHarnessViewModel: ObservableObject {
242247 diagnosticIntervalSeconds: Self . positiveInteger (
243248 environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_DIAGNOSTIC_INTERVAL_SECONDS " ]
244249 ) ?? 60 ,
245- reconnectAfterSeconds: Self . positiveInteger (
250+ reconnectAfterSeconds: Self . nonNegativeInteger (
246251 environment [ " LIVEKIT_NATIVE_DEVICE_HARNESS_RECONNECT_AFTER_SECONDS " ]
247252 )
248253 )
@@ -398,8 +403,11 @@ final class DeviceHarnessViewModel: ObservableObject {
398403 subscriberRemoteAudioPublicationSIDs: subscriberRemotePublications. audio,
399404 subscriberRemoteVideoPublicationSIDs: subscriberRemotePublications. video,
400405 batteryLevel: UIDevice . current. batteryLevel,
406+ batteryState: UIDevice . current. batteryState. benchmarkDescription,
401407 thermalState: thermalState. description,
402- memoryPeakResidentBytes: DeviceHarnessMemorySampler . peakResidentBytes ( )
408+ memoryPeakResidentBytes: DeviceHarnessMemorySampler . peakResidentBytes ( ) ,
409+ appUptimeSeconds: appUptimeSeconds,
410+ processCPUTimeSeconds: DeviceHarnessProcessSampler . cpuTimeSeconds ( )
403411 ) . textReport
404412 emitHarnessLog ( diagnosticsText)
405413 await abortSoakIfThermalStateDisallowed ( thermalState)
@@ -759,10 +767,14 @@ final class DeviceHarnessViewModel: ObservableObject {
759767 if isSoakAutomationRunning {
760768 observedLifecycleEvents. insert ( " background-foreground-recovery " )
761769 await refreshDiagnostics ( reason: " background-foreground-recovery " )
762- scheduleForegroundSoakRecoveryIfNeeded ( initialDiagnostics: diagnostics)
770+ if foregroundRecoveryEnabled {
771+ scheduleForegroundSoakRecoveryIfNeeded ( initialDiagnostics: diagnostics)
772+ }
763773 return
764774 }
765- await recoverAfterForegroundIfNeeded ( initialDiagnostics: diagnostics)
775+ if foregroundRecoveryEnabled {
776+ await recoverAfterForegroundIfNeeded ( initialDiagnostics: diagnostics)
777+ }
766778 observedLifecycleEvents. insert ( " background-foreground-recovery " )
767779 await refreshDiagnostics ( reason: " background-foreground-recovery " )
768780 }
@@ -1058,7 +1070,7 @@ final class DeviceHarnessViewModel: ObservableObject {
10581070 let clampedDurationMinutes = min ( max ( durationMinutes, 1 ) , 24 * 60 )
10591071 let totalSeconds = clampedDurationMinutes * 60
10601072 let intervalSeconds = min ( max ( diagnosticIntervalSeconds, 5 ) , totalSeconds)
1061- let reconnectAtSeconds = min (
1073+ let reconnectAtSeconds = reconnectAfterSeconds == 0 ? nil : min (
10621074 max ( reconnectAfterSeconds ?? max ( 30 , totalSeconds / 2 ) , 5 ) ,
10631075 max ( 5 , totalSeconds - intervalSeconds)
10641076 )
@@ -1093,14 +1105,16 @@ final class DeviceHarnessViewModel: ObservableObject {
10931105 emitHarnessLog ( Self . soakHarnessBuildMarker)
10941106 emitHarnessLog ( soakLowLoadProfileMarker)
10951107 emitHarnessLog (
1096- " soak_started: duration_minutes= \( clampedDurationMinutes) diagnostic_interval_seconds= \( intervalSeconds) reconnect_after_seconds= \( reconnectAtSeconds) "
1108+ " soak_started: duration_minutes= \( clampedDurationMinutes) diagnostic_interval_seconds= \( intervalSeconds) reconnect_after_seconds= \( reconnectAtSeconds. map ( String . init ) ?? " disabled " ) "
10971109 )
10981110 await refreshDiagnostics ( reason: " soak-start " , includeDataChannels: false )
10991111
11001112 while Int ( Date ( ) . timeIntervalSince ( start) ) < totalSeconds {
11011113 let elapsedSeconds = Int ( Date ( ) . timeIntervalSince ( start) )
11021114 var reconnectReason : String ?
1103- if reconnectCount + foregroundSoakReconnectCount == 0 && elapsedSeconds >= reconnectAtSeconds {
1115+ if let reconnectAtSeconds,
1116+ reconnectCount + foregroundSoakReconnectCount == 0 ,
1117+ elapsedSeconds >= reconnectAtSeconds {
11041118 reconnectReason = " scheduled "
11051119 }
11061120
@@ -1251,9 +1265,10 @@ final class DeviceHarnessViewModel: ObservableObject {
12511265 }
12521266
12531267 private var soakLowLoadProfileMarker : String {
1254- let captureProfile = batterySaverWebRTC ? " 320x180@10fps " : " default "
1268+ let captureProfile = batterySaverWebRTC ? " 320x180@10fps " : " 720x1280@30fps "
12551269 let bitrate = batterySaverWebRTC ? " 250000 " : " default "
1256- return " soak_low_load_profile: battery_saver_webrtc= \( batterySaverWebRTC) capture= \( captureProfile) h264_bitrate= \( bitrate) active_video_layers_target=1 decode_subscriber_video=true subscriber_adaptive=false subscriber_allow_pause=false ice_transport_policy= \( allowDirectICE ? " all " : " relay " ) "
1270+ let activeVideoLayersTarget = batterySaverWebRTC ? 1 : 3
1271+ return " soak_low_load_profile: battery_saver_webrtc= \( batterySaverWebRTC) capture= \( captureProfile) h264_bitrate= \( bitrate) active_video_layers_target= \( activeVideoLayersTarget) decode_subscriber_video=true subscriber_adaptive=false subscriber_allow_pause=false ice_transport_policy= \( allowDirectICE ? " all " : " relay " ) "
12571272 }
12581273
12591274 private func reconnectForSoak( reason: String , attempt: Int ) async -> Bool {
@@ -1741,6 +1756,13 @@ final class DeviceHarnessViewModel: ObservableObject {
17411756 return integer
17421757 }
17431758
1759+ private static func nonNegativeInteger( _ value: String ? ) -> Int ? {
1760+ guard let value, let integer = Int ( value) , integer >= 0 else {
1761+ return nil
1762+ }
1763+ return integer
1764+ }
1765+
17441766 private static func joinedOrNone( _ values: [ String ] ) -> String {
17451767 values. isEmpty ? " none " : values. sorted ( ) . joined ( separator: " , " )
17461768 }
@@ -1816,6 +1838,14 @@ final class DeviceHarnessViewModel: ObservableObject {
18161838 " physical-device "
18171839 #endif
18181840 }
1841+
1842+ private var appUptimeSeconds : Double {
1843+ Date ( ) . timeIntervalSince ( appLaunchDate)
1844+ }
1845+
1846+ private static func formatSeconds( _ seconds: Double ) -> String {
1847+ String ( format: " %.3f " , seconds)
1848+ }
18191849}
18201850
18211851private enum DeviceHarnessMemorySampler {
@@ -1828,6 +1858,37 @@ private enum DeviceHarnessMemorySampler {
18281858 }
18291859}
18301860
1861+ private enum DeviceHarnessProcessSampler {
1862+ static func cpuTimeSeconds( ) -> Double {
1863+ var usage = rusage ( )
1864+ guard getrusage ( RUSAGE_SELF, & usage) == 0 else {
1865+ return - 1
1866+ }
1867+ return seconds ( from: usage. ru_utime) + seconds( from: usage. ru_stime)
1868+ }
1869+
1870+ private static func seconds( from time: timeval ) -> Double {
1871+ Double ( time. tv_sec) + Double( time. tv_usec) / 1_000_000
1872+ }
1873+ }
1874+
1875+ private extension UIDevice . BatteryState {
1876+ var benchmarkDescription : String {
1877+ switch self {
1878+ case . unknown:
1879+ return " unknown "
1880+ case . unplugged:
1881+ return " unplugged "
1882+ case . charging:
1883+ return " charging "
1884+ case . full:
1885+ return " full "
1886+ @unknown default :
1887+ return " unknown "
1888+ }
1889+ }
1890+ }
1891+
18311892private struct SubscriberCompanionDiscoveryError : LocalizedError {
18321893 var missingKinds : [ String ]
18331894 var remoteAudioSIDs : [ String ]
0 commit comments