@@ -29,6 +29,7 @@ final class DeviceHarnessViewModel: ObservableObject {
2929 private var didOpenPublisherHarnessDataChannel = false
3030 private var lastDataPublishFailure = " "
3131 private var lastAudioRouteDiagnosticAt = Date . distantPast
32+ private var isSoakAutomationRunning = false
3233 private static let voiceIsolationRecommendationKey =
3334 " LiveKitNativeDeviceHarness.didSuggestAppleVoiceIsolation "
3435
@@ -250,8 +251,11 @@ final class DeviceHarnessViewModel: ObservableObject {
250251 }
251252
252253 @discardableResult
253- func refreshDiagnostics( reason: String ) async -> RoomMediaDiagnostics ? {
254- guard let diagnostics = await currentDiagnostics ( ) else {
254+ func refreshDiagnostics(
255+ reason: String ,
256+ includeDataChannels: Bool = true
257+ ) async -> RoomMediaDiagnostics ? {
258+ guard let diagnostics = await currentDiagnostics ( includeDataChannels: includeDataChannels) else {
255259 diagnosticsText = " No active room for diagnostics. "
256260 return nil
257261 }
@@ -347,14 +351,16 @@ final class DeviceHarnessViewModel: ObservableObject {
347351 }
348352 }
349353
350- private func currentDiagnostics( ) async -> RoomMediaDiagnostics ? {
354+ private func currentDiagnostics( includeDataChannels : Bool = true ) async -> RoomMediaDiagnostics ? {
351355 guard let room else {
352356 return nil
353357 }
354358
355- var diagnostics = await room. mediaDiagnostics ( )
359+ var diagnostics = await room. mediaDiagnostics ( includeDataChannels : includeDataChannels )
356360 if let subscriberRoom {
357- let subscriberDiagnostics = await subscriberRoom. mediaDiagnostics ( )
361+ let subscriberDiagnostics = await subscriberRoom. mediaDiagnostics (
362+ includeDataChannels: includeDataChannels
363+ )
358364 diagnostics = Self . combinedDiagnostics (
359365 publisherDiagnostics: diagnostics,
360366 subscriberDiagnostics: subscriberDiagnostics
@@ -459,6 +465,11 @@ final class DeviceHarnessViewModel: ObservableObject {
459465 }
460466
461467 var policy = WebRTCPowerPolicy . batterySaver
468+ policy. cameraCaptureOptions = CameraCaptureOptions ( width: 320 , height: 180 , framesPerSecond: 10 )
469+ policy. h264Bitrate = 250_000
470+ policy. adaptiveStream = false
471+ policy. subscriberAllowPause = false
472+ policy. applySubscriberAdaptiveTrackSettings = false
462473 policy. decodeSubscriberVideo = true
463474 return . custom( policy)
464475 }
@@ -523,10 +534,17 @@ final class DeviceHarnessViewModel: ObservableObject {
523534 status = " Observed \( name) . "
524535 if name == " audio-route-change " {
525536 let now = Date ( )
526- guard now. timeIntervalSince ( lastAudioRouteDiagnosticAt) >= 5 else {
537+ let minimumInterval : TimeInterval = isSoakAutomationRunning ? 30 : 5
538+ guard now. timeIntervalSince ( lastAudioRouteDiagnosticAt) >= minimumInterval else {
527539 return
528540 }
529541 lastAudioRouteDiagnosticAt = now
542+ if isSoakAutomationRunning {
543+ if let detail {
544+ emitHarnessLog ( detail)
545+ }
546+ return
547+ }
530548 }
531549 if let detail {
532550 emitHarnessLog ( detail)
@@ -742,10 +760,13 @@ final class DeviceHarnessViewModel: ObservableObject {
742760 var didObserveMediaContinuity = false
743761 var lastMediaContinuityCounters : ( renderedFrames: Int , audioBuffers: Int ) ?
744762
763+ isSoakAutomationRunning = true
764+ defer { isSoakAutomationRunning = false }
765+
745766 emitHarnessLog (
746767 " soak_started: duration_minutes= \( clampedDurationMinutes) diagnostic_interval_seconds= \( intervalSeconds) reconnect_after_seconds= \( reconnectAtSeconds) "
747768 )
748- await refreshDiagnostics ( reason: " soak-start " )
769+ await refreshDiagnostics ( reason: " soak-start " , includeDataChannels : false )
749770
750771 while Int ( Date ( ) . timeIntervalSince ( start) ) < totalSeconds {
751772 let elapsedSeconds = Int ( Date ( ) . timeIntervalSince ( start) )
@@ -754,7 +775,10 @@ final class DeviceHarnessViewModel: ObservableObject {
754775 reconnectReason = " scheduled "
755776 }
756777
757- let diagnostics = await refreshDiagnostics ( reason: " media-continuity " )
778+ let diagnostics = await refreshDiagnostics (
779+ reason: " media-continuity " ,
780+ includeDataChannels: false
781+ )
758782 let canAttemptHealthReconnect = lastReconnectElapsedSeconds. map {
759783 elapsedSeconds - $0 >= reconnectCooldownSeconds
760784 } ?? true
@@ -787,16 +811,18 @@ final class DeviceHarnessViewModel: ObservableObject {
787811 reconnectAttemptCount += 1
788812 let attempt = reconnectAttemptCount
789813 emitHarnessLog ( " soak_reconnect: started reason= \( reconnectReason) attempt= \( attempt) elapsed_seconds= \( elapsedSeconds) " )
790- await disconnect ( )
791- await connectAndPublish ( )
814+ let recovered = await reconnectForSoak (
815+ reason: reconnectReason,
816+ attempt: attempt
817+ )
792818 lastReconnectElapsedSeconds = Int ( Date ( ) . timeIntervalSince ( start) )
793- if isConnected {
819+ if recovered {
794820 reconnectCount += 1
795821 }
796822 didObserveMediaContinuity = false
797823 lastMediaContinuityCounters = nil
798- emitHarnessLog ( " soak_reconnect: \( isConnected ? " completed " : " failed " ) reason= \( reconnectReason) attempt= \( attempt) elapsed_seconds= \( Int ( Date ( ) . timeIntervalSince ( start) ) ) " )
799- await refreshDiagnostics ( reason: " reconnect-recovery " )
824+ emitHarnessLog ( " soak_reconnect: \( recovered ? " completed " : " failed " ) reason= \( reconnectReason) attempt= \( attempt) elapsed_seconds= \( Int ( Date ( ) . timeIntervalSince ( start) ) ) " )
825+ await refreshDiagnostics ( reason: " reconnect-recovery " , includeDataChannels : false )
800826 }
801827
802828 let remainingSeconds = max ( 1 , totalSeconds - Int( Date ( ) . timeIntervalSince ( start) ) )
@@ -806,7 +832,28 @@ final class DeviceHarnessViewModel: ObservableObject {
806832
807833 emitHarnessLog ( " duration_minutes: \( clampedDurationMinutes) " )
808834 emitHarnessLog ( " soak_completed: duration_minutes= \( clampedDurationMinutes) reconnect_observed= \( reconnectCount > 0 ) reconnect_count= \( reconnectCount) reconnect_attempts= \( reconnectAttemptCount) " )
809- await refreshDiagnostics ( reason: " soak-complete " )
835+ var completionReady = soakCompletionDiagnosticsReady (
836+ await refreshDiagnostics ( reason: " soak-complete " , includeDataChannels: false )
837+ )
838+ if !completionReady {
839+ for attempt in 1 ... 3 {
840+ emitHarnessLog ( " soak_complete_recovery: reconnecting attempt= \( attempt) " )
841+ _ = await reconnectForSoak (
842+ reason: " soak-complete " ,
843+ attempt: attempt
844+ )
845+ try ? await Task . sleep ( nanoseconds: 5_000_000_000 )
846+ completionReady = soakCompletionDiagnosticsReady (
847+ await refreshDiagnostics ( reason: " soak-complete " , includeDataChannels: false )
848+ )
849+ if completionReady {
850+ break
851+ }
852+ }
853+ if !completionReady {
854+ emitHarnessLog ( " soak_complete_recovery: failed " )
855+ }
856+ }
810857 }
811858
812859 private func shouldRecoverDuringSoak( diagnostics: RoomMediaDiagnostics ) -> Bool {
@@ -824,6 +871,101 @@ final class DeviceHarnessViewModel: ObservableObject {
824871 return false
825872 }
826873
874+ private func reconnectForSoak( reason: String , attempt: Int ) async -> Bool {
875+ for retry in 1 ... 3 {
876+ disconnectCurrentRoomsInBackground ( )
877+ try ? await Task . sleep ( nanoseconds: UInt64 ( retry) * 2_000_000_000 )
878+ let connected = await connectAndPublishForSoak ( timeoutSeconds: 45 )
879+ if connected {
880+ return true
881+ }
882+ if retry < 3 {
883+ emitHarnessLog ( " soak_reconnect_retry: reason= \( reason) attempt= \( attempt) retry= \( retry) " )
884+ }
885+ }
886+
887+ return false
888+ }
889+
890+ private func connectAndPublishForSoak( timeoutSeconds: UInt64 ) async -> Bool {
891+ await connectAndPublish ( disconnectFirst: false )
892+ guard isConnected else {
893+ return false
894+ }
895+
896+ let deadline = Date ( ) . addingTimeInterval ( TimeInterval ( timeoutSeconds) )
897+ while Date ( ) < deadline {
898+ if let diagnostics = await currentDiagnostics ( includeDataChannels: false ) ,
899+ soakReconnectDiagnosticsReady ( diagnostics)
900+ {
901+ return true
902+ }
903+ try ? await Task . sleep ( nanoseconds: 1_000_000_000 )
904+ }
905+
906+ emitHarnessLog ( " soak_reconnect_timeout: seconds= \( timeoutSeconds) " )
907+ disconnectCurrentRoomsInBackground ( )
908+ return false
909+ }
910+
911+ private func soakReconnectDiagnosticsReady( _ diagnostics: RoomMediaDiagnostics ) -> Bool {
912+ if !soakMediaPathConnected( diagnostics) {
913+ return false
914+ }
915+ if diagnostics. counters. publisherAudioSenderCount < 1 ||
916+ diagnostics. counters. publisherVideoSenderCount < 1
917+ {
918+ return false
919+ }
920+ if diagnostics. counters. subscriberRenderedVideoFrameCount < 1 ||
921+ diagnostics. counters. subscriberAudioPlayoutScheduledBufferCount < 1
922+ {
923+ return false
924+ }
925+
926+ return true
927+ }
928+
929+ private func soakMediaPathConnected( _ diagnostics: RoomMediaDiagnostics ) -> Bool {
930+ if diagnostics. connectionState != . connected {
931+ return false
932+ }
933+ if diagnostics. publisher. dtlsSRTPState != . connected {
934+ return false
935+ }
936+ if !subscriberToken. isEmpty,
937+ diagnostics. subscriber. dtlsSRTPState != . connected
938+ {
939+ return false
940+ }
941+
942+ return true
943+ }
944+
945+ private func soakCompletionDiagnosticsReady( _ diagnostics: RoomMediaDiagnostics ? ) -> Bool {
946+ guard let diagnostics else {
947+ return false
948+ }
949+
950+ let counters = diagnostics. counters
951+ let renderedFrames = max (
952+ counters. subscriberRenderedVideoFrameCount,
953+ videoView. renderedFrameCount
954+ )
955+
956+ if !soakMediaPathConnected( diagnostics) {
957+ return false
958+ }
959+ if counters. publisherAudioSenderCount < 1 || counters. publisherVideoSenderCount < 1 {
960+ return false
961+ }
962+ if renderedFrames < 1 || counters. subscriberAudioPlayoutScheduledBufferCount < 1 {
963+ return false
964+ }
965+
966+ return true
967+ }
968+
827969 private func emitHarnessLog( _ text: String ) {
828970 print ( text)
829971 }
0 commit comments