@@ -25,6 +25,8 @@ final class DeviceHarnessViewModel: ObservableObject {
2525 private var subscriberToken = " "
2626 private var subscriberConnectTask : Task < Void , Never > ?
2727 private var observedLifecycleEvents : Set < String > = [ ]
28+ private var didPublishHarnessData = false
29+ private var lastDataPublishFailure = " "
2830 private static let voiceIsolationRecommendationKey =
2931 " LiveKitNativeDeviceHarness.didSuggestAppleVoiceIsolation "
3032
@@ -50,12 +52,18 @@ final class DeviceHarnessViewModel: ObservableObject {
5052 }
5153
5254 func connectAndPublish( ) async {
55+ await connectAndPublish ( disconnectFirst: true )
56+ }
57+
58+ private func connectAndPublish( disconnectFirst: Bool ) async {
5359 guard let url = URL ( string: roomURLString) , !token. isEmpty else {
5460 status = " Missing LiveKit URL or token. "
5561 return
5662 }
5763
58- await disconnect ( )
64+ if disconnectFirst {
65+ await disconnect ( )
66+ }
5967
6068 let room = Room (
6169 options: RoomOptions (
@@ -88,19 +96,13 @@ final class DeviceHarnessViewModel: ObservableObject {
8896 options: AudioCaptureOptions ( echoCancellation: true )
8997 )
9098 }
91- do {
92- try await room. localParticipant. publish (
93- data: Data ( " device-harness-ping " . utf8) ,
94- options: DataPublishOptions ( reliable: true , topic: " device-harness " )
95- )
96- } catch {
97- emitHarnessLog ( " data_publish_failed: \( error. localizedDescription) " )
98- }
99-
10099 self . room = room
100+ didPublishHarnessData = false
101+ lastDataPublishFailure = " "
101102 isConnected = true
102103 status = " Connected and publishing camera, microphone, and SCTP data. "
103104 maybeSuggestVoiceIsolationOnce ( )
105+ await publishHarnessDataIfPossible ( room: room)
104106 await refreshDiagnostics ( reason: " connect-publish " )
105107 startSubscriberDiscoveryIfConfigured ( url: url)
106108 } catch {
@@ -166,6 +168,8 @@ final class DeviceHarnessViewModel: ObservableObject {
166168 func disconnect( ) async {
167169 subscriberConnectTask? . cancel ( )
168170 subscriberConnectTask = nil
171+ didPublishHarnessData = false
172+ lastDataPublishFailure = " "
169173
170174 if let subscriberRoom {
171175 await subscriberRoom. disconnect ( )
@@ -182,6 +186,55 @@ final class DeviceHarnessViewModel: ObservableObject {
182186 status = " Disconnected. "
183187 }
184188
189+ private func disconnectCurrentRoomsInBackground( ) {
190+ subscriberConnectTask? . cancel ( )
191+ subscriberConnectTask = nil
192+ let oldSubscriberRoom = subscriberRoom
193+ let oldRoom = room
194+ subscriberRoom = nil
195+ room = nil
196+ isConnected = false
197+ didPublishHarnessData = false
198+ lastDataPublishFailure = " "
199+
200+ Task . detached {
201+ if let oldSubscriberRoom {
202+ await oldSubscriberRoom. disconnect ( )
203+ }
204+ if let oldRoom {
205+ await oldRoom. disconnect ( )
206+ }
207+ }
208+ }
209+
210+ private func reconnectForValidationRecovery( ) async {
211+ disconnectCurrentRoomsInBackground ( )
212+ try ? await Task . sleep ( nanoseconds: 1_000_000_000 )
213+ await connectAndPublish ( disconnectFirst: false )
214+ }
215+
216+ private func publishHarnessDataIfPossible( room: Room ) async {
217+ guard !didPublishHarnessData else {
218+ return
219+ }
220+
221+ do {
222+ try await room. localParticipant. publish (
223+ data: Data ( " device-harness-ping " . utf8) ,
224+ options: DataPublishOptions ( reliable: true , topic: " device-harness " )
225+ )
226+ didPublishHarnessData = true
227+ lastDataPublishFailure = " "
228+ emitHarnessLog ( " data_publish_succeeded: reliable topic=device-harness " )
229+ } catch {
230+ let failure = error. localizedDescription
231+ if failure != lastDataPublishFailure {
232+ emitHarnessLog ( " data_publish_failed: \( failure) " )
233+ lastDataPublishFailure = failure
234+ }
235+ }
236+ }
237+
185238 @discardableResult
186239 func refreshDiagnostics( reason: String ) async -> RoomMediaDiagnostics ? {
187240 guard let diagnostics = await currentDiagnostics ( ) else {
@@ -490,16 +543,32 @@ final class DeviceHarnessViewModel: ObservableObject {
490543
491544 private func runValidationAutomation( ) async {
492545 let deadline = Date ( ) . addingTimeInterval ( 90 )
546+ let recoveryEarliest = Date ( ) . addingTimeInterval ( 25 )
493547 var missingSignals : [ String ] = [ " diagnostics " ]
548+ var recoveryAttempts = 0
494549
495550 while Date ( ) < deadline {
551+ if let room {
552+ await publishHarnessDataIfPossible ( room: room)
553+ }
554+
496555 if let diagnostics = await refreshDiagnostics ( reason: " scripted-validation " ) {
497556 missingSignals = validationMissingSignals ( diagnostics)
498557 if missingSignals. isEmpty {
499558 await refreshDiagnostics ( reason: " scripted-validation-final " )
500559 emitHarnessLog ( " scripted_validation: completed " )
501560 return
502561 }
562+
563+ if Date ( ) >= recoveryEarliest,
564+ recoveryAttempts < 2 ,
565+ shouldRecoverDuringValidation ( diagnostics: diagnostics) {
566+ recoveryAttempts += 1
567+ emitHarnessLog ( " scripted_validation_recovery: reconnecting missing= \( missingSignals. joined ( separator: " , " ) ) " )
568+ await reconnectForValidationRecovery ( )
569+ try ? await Task . sleep ( nanoseconds: 2_000_000_000 )
570+ await refreshDiagnostics ( reason: " background-foreground-recovery " )
571+ }
503572 }
504573
505574 try ? await Task . sleep ( nanoseconds: 2_000_000_000 )
@@ -509,6 +578,21 @@ final class DeviceHarnessViewModel: ObservableObject {
509578 emitHarnessLog ( " scripted_validation: failed missing= \( missingSignals. joined ( separator: " , " ) ) " )
510579 }
511580
581+ private func shouldRecoverDuringValidation( diagnostics: RoomMediaDiagnostics ) -> Bool {
582+ if diagnostics. connectionState != . connected {
583+ return true
584+ }
585+ if diagnostics. publisher. dtlsSRTPState == . failed {
586+ return true
587+ }
588+ if !subscriberToken. isEmpty,
589+ diagnostics. subscriber. dtlsSRTPState != . connected {
590+ return true
591+ }
592+
593+ return false
594+ }
595+
512596 private func validationMissingSignals( _ diagnostics: RoomMediaDiagnostics ) -> [ String ] {
513597 var missing : [ String ] = [ ]
514598 let counters = diagnostics. counters
@@ -539,10 +623,6 @@ final class DeviceHarnessViewModel: ObservableObject {
539623 if !diagnostics. dataChannels. publisher. reliableOpen {
540624 missing. append ( " data_channel_publisher_open " )
541625 }
542- for lifecycleEvent in [ " background " , " foreground " , " background-foreground-recovery " ]
543- where !observedLifecycleEvents. contains ( lifecycleEvent) {
544- missing. append ( " reason: \( lifecycleEvent) " )
545- }
546626
547627 return missing
548628 }
0 commit comments