Post

Replies

Boosts

Views

Activity

Reply to Supported way to re-acquire genlock after follow() detaches mid-session?
Configuration detail and the measurements behind the numbers in the original post. Session configuration. One plain AVCaptureSession; AVCaptureMultiCamSession is not used anywhere in the app. One input (.builtInWideAngleCamera, back), one AVCaptureVideoDataOutput, no depth output, no AVCaptureDataOutputSynchronizer, no audio input in this session. activeVideoMinFrameDuration == activeVideoMaxFrameDuration == CMTime(1, 30), set before the AVCaptureDeviceInput is created and never rewritten while a follow is live. preferredVideoStabilizationMode = .off, alwaysDiscardsLateVideoFrames = false. Preflight, logged immediately before every follow(), verbatim from Device A: follow preflight: format=4032x3024 extSyncSupported=true minSyncDur=1/60 (valid=true) autoVFR=false follow preflight: format=1920x1440 extSyncSupported=true minSyncDur=1/60 (valid=true) autoVFR=false The Device B preflight line was not retained — same binary, same code path. Reference signal. Two configurations were exercised; all drop data is from (2). An output labelled "FHD 29.97" that empirically pulses at 59.94 Hz. Followed with CMTime(1001, 60000) against 1920x1080 it locks in ~2 s and the sensor runs at half rate (measured interval 33.330 ms ± 0.786 ms). Followed at a nominal 29.97 it never leaves .calibrating and the delegate reports back .ready, matching the documented "not close enough" behavior. A generator confirmed to output a true, exact 30.00 fps, followed with CMTime(1, 30). Single-variable isolation, Device B (26.5.2). All cold .nominal starts, all recording. 4032x3024 + in-session mic + live preview drop at 6 s (.nominal / .nominal) 1920x1080 + preview off + in-session mic drop at 37 s (.nominal / .fair) 1920x1080 + preview off + mic off 211.9 s / 6358 frames, LOCK HELD, zero drops 4032x3024 + preview off + mic off drop at 102 s (.nominal / .nominal) Removing the mic input took the 1080p case from a 37 s drop to a run held end to end, so session load is a strong lever on how long the lock survives. But with recording off, audio off and all other in-app collection disabled, at 1920x1440, the follow still dropped, and detaches also occur during idle follow with no recording at all. Device A flaps. No .activeSync → .ready + nil detach has ever been recorded on this unit. Its only interruptions are flaps where the follow stays attached and the status returns on its own — 7 in one day's log, all ~1 s except one ~22 s, all while not recording: [07:12:25] status: LOCKED → FREE-RUN [07:12:26] status: FREE-RUN → LOCKED [07:57:22] transient UNAVAILABLE after lock — follow still attached, staying locked [07:57:22] status: UNAVAILABLE → FREE-RUN [07:57:44] status: FREE-RUN → LOCKED Device B takes the coast sometimes too: one cold-start .activeSync → .unavailable → .freeRunSync → .activeSync recovery in ~1 s, follow still attached, no flash. But no confirmed drop has ever gone through .freeRunSync. Head-to-head. One directly paired run, identical binary and configuration (4032x3024, 30 fps, HEVC 8-bit, Apple Log off), both recording, both at .serious: Device A completed the full 601 s window with zero drops, Device B dropped at 13 s. I do not treat 13 s as a clean number — that Device B process had already churned through a previous drop in the same launch. A freshly launched process on B has held 9–11 minutes. Wedge state, system-level and cross-app. A crash or force-quit while sync is engaged, or an activeFormat change mid-sync, leaves the device in a state where no app — including Blackmagic Camera — can calibrate afterwards. A ProDock power cycle does not clear it; only rebooting the iPhone does. Every measurement above comes from a session that first demonstrated a clean ~2 s lock, so none was taken inside a wedge window.
Topic: Media Technologies SubTopic: Video Tags:
17h
Reply to Supported way to re-acquire genlock after follow() detaches mid-session?
Code and the transition log, which would not fit in the original post. Transition (condensed; LOCKED = .activeSync) t=0.0 s .unavailable → .ready → .calibrating → .activeSync (~2 s after startRunning()) ... locked; every frame PTS exactly on the 1/30 grid t=T .activeSync → .ready, input.externalSyncDevice == nil t=T+0.30 s re-sampled, still nil → app declares the lock lost externalSyncDevice(_:failedWithError:) is implemented and logs domain#code; neither that log line nor the forced .unavailable transition my handler applies ever appears at a drop. That is inferred from absence, not from a positive trace. I did not record the ProDock REF indicator state at the drop instant. A few frames of exposure / white-balance flash are visible at the drop, reproducible with fixed WB — consistent with an input-port or clock reset, but not directly instrumented. Code. Reduced to the three methods that matter; helper names are sample names, not API. The class is main-actor isolated, and every KVO and delegate callback arrives outside that isolation and hops in through Task { @MainActor in ... }, so callbacks are delivered asynchronously and their relative ordering is not guaranteed. If the mistake is in my sequence, I expect it is here. private func handleStatus(_ status: AVExternalSyncDevice.Status) { if status == .activeSync { wasLocked = true; graceTimer?.invalidate(); graceTimer = nil; return } // .freeRunSync after .activeSync is tolerated as a coast: falls through untouched. guard status == .ready || status == .unavailable else { return } guard input.externalSyncDevice == nil else { return } // still attached: flap, ignore if wasLocked { scheduleRecheck(after: 0.30) } else { follow() } } // follow() with status != .ready or isExternalSyncSupported == false raises an // NSInvalidArgumentException I cannot catch from Swift, so both are re-checked. @discardableResult private func follow() -> Bool { guard let dev = syncDevice, dev.status == .ready else { return false } guard input.isExternalSyncSupported else { return false } input.follow(dev, videoFrameDuration: frameDuration, delegate: self) return true } // One nil sample is ambiguous, so it is confirmed over a grace window. private func scheduleRecheck(after seconds: TimeInterval) { graceTimer?.invalidate() graceTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { [weak self] _ in Task { @MainActor in guard let self else { return } self.graceTimer = nil guard self.input.externalSyncDevice == nil else { return } // self-healed await self.recover() } } } // Recovery, deliberately without touching activeFormat. private func recover() async { input.unfollowExternalSyncDevice() try? await Task.sleep(nanoseconds: 800_000_000) session.stopRunning(); session.startRunning() try? await Task.sleep(nanoseconds: 500_000_000) follow() } Status arrives twice — once through KVO on AVExternalSyncDevice.status, once through externalSyncDeviceStatusDidChange(_:). Both funnel into handleStatus. The app never writes its own status value; every transition reported came from KVO or the delegate. recover() never changes activeFormat: a format change after any follow() in the same process crashes for me — a separate issue I am not bundling into this thread. Grace window. 0.30 s, settled on empirically. Widened to 1.0 s it produced 0 self-heals in 5 drops, so I reverted. Once the sample reads nil it stays nil until an unfollow + bounce.
Topic: Media Technologies SubTopic: Video Tags:
17h
Reply to Supported way to re-acquire genlock after follow() detaches mid-session?
Configuration detail and the measurements behind the numbers in the original post. Session configuration. One plain AVCaptureSession; AVCaptureMultiCamSession is not used anywhere in the app. One input (.builtInWideAngleCamera, back), one AVCaptureVideoDataOutput, no depth output, no AVCaptureDataOutputSynchronizer, no audio input in this session. activeVideoMinFrameDuration == activeVideoMaxFrameDuration == CMTime(1, 30), set before the AVCaptureDeviceInput is created and never rewritten while a follow is live. preferredVideoStabilizationMode = .off, alwaysDiscardsLateVideoFrames = false. Preflight, logged immediately before every follow(), verbatim from Device A: follow preflight: format=4032x3024 extSyncSupported=true minSyncDur=1/60 (valid=true) autoVFR=false follow preflight: format=1920x1440 extSyncSupported=true minSyncDur=1/60 (valid=true) autoVFR=false The Device B preflight line was not retained — same binary, same code path. Reference signal. Two configurations were exercised; all drop data is from (2). An output labelled "FHD 29.97" that empirically pulses at 59.94 Hz. Followed with CMTime(1001, 60000) against 1920x1080 it locks in ~2 s and the sensor runs at half rate (measured interval 33.330 ms ± 0.786 ms). Followed at a nominal 29.97 it never leaves .calibrating and the delegate reports back .ready, matching the documented "not close enough" behavior. A generator confirmed to output a true, exact 30.00 fps, followed with CMTime(1, 30). Single-variable isolation, Device B (26.5.2). All cold .nominal starts, all recording. 4032x3024 + in-session mic + live preview drop at 6 s (.nominal / .nominal) 1920x1080 + preview off + in-session mic drop at 37 s (.nominal / .fair) 1920x1080 + preview off + mic off 211.9 s / 6358 frames, LOCK HELD, zero drops 4032x3024 + preview off + mic off drop at 102 s (.nominal / .nominal) Removing the mic input took the 1080p case from a 37 s drop to a run held end to end, so session load is a strong lever on how long the lock survives. But with recording off, audio off and all other in-app collection disabled, at 1920x1440, the follow still dropped, and detaches also occur during idle follow with no recording at all. Device A flaps. No .activeSync → .ready + nil detach has ever been recorded on this unit. Its only interruptions are flaps where the follow stays attached and the status returns on its own — 7 in one day's log, all ~1 s except one ~22 s, all while not recording: [07:12:25] status: LOCKED → FREE-RUN [07:12:26] status: FREE-RUN → LOCKED [07:57:22] transient UNAVAILABLE after lock — follow still attached, staying locked [07:57:22] status: UNAVAILABLE → FREE-RUN [07:57:44] status: FREE-RUN → LOCKED Device B takes the coast sometimes too: one cold-start .activeSync → .unavailable → .freeRunSync → .activeSync recovery in ~1 s, follow still attached, no flash. But no confirmed drop has ever gone through .freeRunSync. Head-to-head. One directly paired run, identical binary and configuration (4032x3024, 30 fps, HEVC 8-bit, Apple Log off), both recording, both at .serious: Device A completed the full 601 s window with zero drops, Device B dropped at 13 s. I do not treat 13 s as a clean number — that Device B process had already churned through a previous drop in the same launch. A freshly launched process on B has held 9–11 minutes. Wedge state, system-level and cross-app. A crash or force-quit while sync is engaged, or an activeFormat change mid-sync, leaves the device in a state where no app — including Blackmagic Camera — can calibrate afterwards. A ProDock power cycle does not clear it; only rebooting the iPhone does. Every measurement above comes from a session that first demonstrated a clean ~2 s lock, so none was taken inside a wedge window.
Topic: Media Technologies SubTopic: Video Tags:
Replies
Boosts
Views
Activity
17h
Reply to Supported way to re-acquire genlock after follow() detaches mid-session?
Code and the transition log, which would not fit in the original post. Transition (condensed; LOCKED = .activeSync) t=0.0 s .unavailable → .ready → .calibrating → .activeSync (~2 s after startRunning()) ... locked; every frame PTS exactly on the 1/30 grid t=T .activeSync → .ready, input.externalSyncDevice == nil t=T+0.30 s re-sampled, still nil → app declares the lock lost externalSyncDevice(_:failedWithError:) is implemented and logs domain#code; neither that log line nor the forced .unavailable transition my handler applies ever appears at a drop. That is inferred from absence, not from a positive trace. I did not record the ProDock REF indicator state at the drop instant. A few frames of exposure / white-balance flash are visible at the drop, reproducible with fixed WB — consistent with an input-port or clock reset, but not directly instrumented. Code. Reduced to the three methods that matter; helper names are sample names, not API. The class is main-actor isolated, and every KVO and delegate callback arrives outside that isolation and hops in through Task { @MainActor in ... }, so callbacks are delivered asynchronously and their relative ordering is not guaranteed. If the mistake is in my sequence, I expect it is here. private func handleStatus(_ status: AVExternalSyncDevice.Status) { if status == .activeSync { wasLocked = true; graceTimer?.invalidate(); graceTimer = nil; return } // .freeRunSync after .activeSync is tolerated as a coast: falls through untouched. guard status == .ready || status == .unavailable else { return } guard input.externalSyncDevice == nil else { return } // still attached: flap, ignore if wasLocked { scheduleRecheck(after: 0.30) } else { follow() } } // follow() with status != .ready or isExternalSyncSupported == false raises an // NSInvalidArgumentException I cannot catch from Swift, so both are re-checked. @discardableResult private func follow() -> Bool { guard let dev = syncDevice, dev.status == .ready else { return false } guard input.isExternalSyncSupported else { return false } input.follow(dev, videoFrameDuration: frameDuration, delegate: self) return true } // One nil sample is ambiguous, so it is confirmed over a grace window. private func scheduleRecheck(after seconds: TimeInterval) { graceTimer?.invalidate() graceTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { [weak self] _ in Task { @MainActor in guard let self else { return } self.graceTimer = nil guard self.input.externalSyncDevice == nil else { return } // self-healed await self.recover() } } } // Recovery, deliberately without touching activeFormat. private func recover() async { input.unfollowExternalSyncDevice() try? await Task.sleep(nanoseconds: 800_000_000) session.stopRunning(); session.startRunning() try? await Task.sleep(nanoseconds: 500_000_000) follow() } Status arrives twice — once through KVO on AVExternalSyncDevice.status, once through externalSyncDeviceStatusDidChange(_:). Both funnel into handleStatus. The app never writes its own status value; every transition reported came from KVO or the delegate. recover() never changes activeFormat: a format change after any follow() in the same process crashes for me — a separate issue I am not bundling into this thread. Grace window. 0.30 s, settled on empirically. Widened to 1.0 s it produced 0 self-heals in 5 drops, so I reverted. Once the sample reads nil it stays nil until an unfollow + bounce.
Topic: Media Technologies SubTopic: Video Tags:
Replies
Boosts
Views
Activity
17h