From 4eb6f399db4fde261e40c8992a37fe31801b8afe Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Mon, 10 Aug 2026 22:43:35 -0700 Subject: [PATCH] Multicam scaffolding: feature flag, supports_multicam capability, sync metadata helpers Inert groundwork for the multicam director feature (PR0 of the plan): - FeatureFlags.ENABLE_MULTICAM=false master switch; cameras advertise the new supports_multicam capability tied to the flag, so the same release that flips it on starts advertising. - FlatBufferSchemas.fbs: supports_multicam appended to CameraCapabilities (same evolution pattern as supports_focus_point), regenerated with flatc, round-tripped in RemoteCmdSerializationTests including the legacy-peer absent-field default. - CaptureSyncMetadata: the per-clip alignment record (shared director-clock anchor + captureId/sessionId + offset quality) with filename prefix, QuickTime metadata items, and deterministic JSON; unused until synced capture ships. No behavior change; full suite green. Co-Authored-By: Claude Fable 5 --- RemoteCam/CaptureEngine.swift | 3 + RemoteCam/CaptureSyncMetadata.swift | 88 +++++++++++++++++++ RemoteCam/FeatureFlags.swift | 6 ++ RemoteCam/FlatBufferSchemas.fbs | 5 ++ RemoteCam/FlatBufferSchemas_generated.swift | 11 ++- RemoteCam/RemoteCmdFlatBuffers.swift | 4 +- RemoteCam/RemoteCmds.swift | 6 ++ RemoteCamTests/CaptureSyncMetadataTests.swift | 73 +++++++++++++++ .../RemoteCmdSerializationTests.swift | 16 ++++ RemoteShutter.xcodeproj/project.pbxproj | 8 ++ 10 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 RemoteCam/CaptureSyncMetadata.swift create mode 100644 RemoteCamTests/CaptureSyncMetadataTests.swift diff --git a/RemoteCam/CaptureEngine.swift b/RemoteCam/CaptureEngine.swift index 405eb268..76019483 100644 --- a/RemoteCam/CaptureEngine.swift +++ b/RemoteCam/CaptureEngine.swift @@ -938,6 +938,9 @@ final class CaptureEngine: NSObject, AVCapturePhotoCaptureDelegate { // This build understands SetCameraPreviewMode; advertise the current // persisted mode so the monitor reflects it from the first exchange. supportsPreviewMode: true, + // Tied to the flag so cameras start advertising multicam the same + // release the director UI ships. + supportsMulticam: FeatureFlags.ENABLE_MULTICAM, previewMode: CameraPreviewModeStore().load(), error: nil ) diff --git a/RemoteCam/CaptureSyncMetadata.swift b/RemoteCam/CaptureSyncMetadata.swift new file mode 100644 index 00000000..376e34a5 --- /dev/null +++ b/RemoteCam/CaptureSyncMetadata.swift @@ -0,0 +1,88 @@ +// +// CaptureSyncMetadata.swift +// RemoteShutter +// +// Created by Dario Lencina on 2026. +// Copyright © 2026 Security Union. All rights reserved. +// + +import AVFoundation +import Foundation + +/// Alignment metadata attached to every clip and photo captured in a multicam +/// director session. Each camera saves full-res media locally; these fields +/// are what let any editor (CapCut, FCP, Resolve) line the angles up without +/// the files ever leaving the phones. +/// +/// The `anchorMillis` timestamp is the scheduled fire time on the *director's* +/// clock, so it is identical across all N cameras' clips for one capture — +/// that shared value is the alignment key. `clockOffsetMillis`/ +/// `roundTripMillis` record how good this camera's clock estimate was when it +/// fired (a quality hint, not part of alignment). +struct CaptureSyncMetadata: Codable, Equatable { + /// One per multicam rig session (director generates it). + let sessionID: String + /// One per shutter press / record start, shared by every camera in the rig. + let captureID: String + /// Stable 1-based index of this camera in the rig, for humans and filenames. + let cameraIndex: Int + /// Scheduled fire time in ms on the director's clock — the alignment key. + let anchorMillis: UInt64 + /// This camera's estimated clock offset vs the director when it fired (ms). + let clockOffsetMillis: Int64 + /// RTT of the offset estimate (ms); smaller = tighter sync. + let roundTripMillis: Int64 + + /// QuickTime metadata keys, reverse-DNS in the `mdta` keyspace. + enum QuickTimeKey { + static let anchor = "com.remoteshutter.syncAnchorMs" + static let capture = "com.remoteshutter.captureId" + static let session = "com.remoteshutter.sessionId" + static let offset = "com.remoteshutter.clockOffsetMs" + } + + /// `RS___cam` — groups one capture's files across cameras + /// when they land in a shared folder or an editor's media bin. Uses the + /// first UUID group so names stay readable. + var filenamePrefix: String { + "RS_\(Self.shortID(sessionID))_\(Self.shortID(captureID))_cam\(cameraIndex)" + } + + /// Items for `AVAssetWriter.metadata` so the values travel inside the + /// .mov itself and survive export/AirDrop. + func quickTimeMetadataItems() -> [AVMetadataItem] { + [ + Self.item(key: QuickTimeKey.anchor, value: NSNumber(value: anchorMillis)), + Self.item(key: QuickTimeKey.capture, value: captureID as NSString), + Self.item(key: QuickTimeKey.session, value: sessionID as NSString), + Self.item(key: QuickTimeKey.offset, value: NSNumber(value: clockOffsetMillis)), + ] + } + + /// JSON blob for the photo path (EXIF UserComment) and the Documents + /// sidecar. Sorted keys so output is deterministic and testable. + func jsonString() -> String? { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + guard let data = try? encoder.encode(self) else { return nil } + return String(data: data, encoding: .utf8) + } + + static func fromJSONString(_ string: String) -> CaptureSyncMetadata? { + guard let data = string.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode(CaptureSyncMetadata.self, from: data) + } + + private static func shortID(_ uuidString: String) -> String { + String(uuidString.prefix(8)).lowercased() + } + + private static func item(key: String, value: NSCopying & NSObjectProtocol) -> AVMetadataItem { + let item = AVMutableMetadataItem() + item.identifier = AVMetadataItem.identifier(forKey: key, keySpace: .quickTimeMetadata) + item.keySpace = .quickTimeMetadata + item.key = key as NSString + item.value = value + return item + } +} diff --git a/RemoteCam/FeatureFlags.swift b/RemoteCam/FeatureFlags.swift index b9541695..de704651 100644 --- a/RemoteCam/FeatureFlags.swift +++ b/RemoteCam/FeatureFlags.swift @@ -31,6 +31,12 @@ struct FeatureFlags { /// one-time buy, and the entitlement code stays in place for when it flips on. static let ENABLE_PRO_SUBSCRIPTION = false + /// Multicam director mode: one monitor controlling several cameras with + /// synced capture. Off until the feature ships (target 9.1.0); while off, + /// cameras advertise `supports_multicam=false` and the scanner keeps its + /// single-camera flow. + static let ENABLE_MULTICAM = false + /// Show the local camera-device picker on the camera screen. On for Mac /// Catalyst only (a Mac has N cameras — built-in, Continuity, USB); /// iPhone keeps its flip button. diff --git a/RemoteCam/FlatBufferSchemas.fbs b/RemoteCam/FlatBufferSchemas.fbs index 303bf1ea..99e6f4f6 100644 --- a/RemoteCam/FlatBufferSchemas.fbs +++ b/RemoteCam/FlatBufferSchemas.fbs @@ -257,6 +257,11 @@ table CameraCapabilities { // not send SetCameraPreviewMode to such a peer (old decoders read the // unknown action as its enum default). supports_preview_mode: bool; + // False/absent = peer cannot join a multicam director session; a director + // must not send scheduled-capture or stream-profile commands to such a + // peer (they would be decoded as Unknown and dropped, silently desyncing + // the rig). + supports_multicam: bool; } // MARK: - Response Structure diff --git a/RemoteCam/FlatBufferSchemas_generated.swift b/RemoteCam/FlatBufferSchemas_generated.swift index 951f0efd..f2859ac2 100644 --- a/RemoteCam/FlatBufferSchemas_generated.swift +++ b/RemoteCam/FlatBufferSchemas_generated.swift @@ -1011,6 +1011,7 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { case activeDeviceId = 10 case supportsFocusPoint = 12 case supportsPreviewMode = 14 + case supportsMulticam = 16 var v: Int32 { Int32(self.rawValue) } var p: VOffset { self.rawValue } } @@ -1024,7 +1025,8 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { public var activeDeviceIdSegmentArray: [UInt8]? { return _accessor.getVector(at: VTOFFSET.activeDeviceId.v) } public var supportsFocusPoint: Bool { let o = _accessor.offset(VTOFFSET.supportsFocusPoint.v); return o == 0 ? false : _accessor.readBuffer(of: Bool.self, at: o) } public var supportsPreviewMode: Bool { let o = _accessor.offset(VTOFFSET.supportsPreviewMode.v); return o == 0 ? false : _accessor.readBuffer(of: Bool.self, at: o) } - public static func startCameraCapabilities(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 6) } + public var supportsMulticam: Bool { let o = _accessor.offset(VTOFFSET.supportsMulticam.v); return o == 0 ? false : _accessor.readBuffer(of: Bool.self, at: o) } + public static func startCameraCapabilities(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 7) } public static func add(frontCamera: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: frontCamera, at: VTOFFSET.frontCamera.p) } public static func add(backCamera: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: backCamera, at: VTOFFSET.backCamera.p) } public static func addVectorOf(cameraDevices: Offset, _ fbb: inout FlatBufferBuilder) { fbb.add(offset: cameraDevices, at: VTOFFSET.cameraDevices.p) } @@ -1033,6 +1035,8 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { at: VTOFFSET.supportsFocusPoint.p) } public static func add(supportsPreviewMode: Bool, _ fbb: inout FlatBufferBuilder) { fbb.add(element: supportsPreviewMode, def: false, at: VTOFFSET.supportsPreviewMode.p) } + public static func add(supportsMulticam: Bool, _ fbb: inout FlatBufferBuilder) { fbb.add(element: supportsMulticam, def: false, + at: VTOFFSET.supportsMulticam.p) } public static func endCameraCapabilities(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } public static func createCameraCapabilities( _ fbb: inout FlatBufferBuilder, @@ -1041,7 +1045,8 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { cameraDevicesVectorOffset cameraDevices: Offset = Offset(), activeDeviceIdOffset activeDeviceId: Offset = Offset(), supportsFocusPoint: Bool = false, - supportsPreviewMode: Bool = false + supportsPreviewMode: Bool = false, + supportsMulticam: Bool = false ) -> Offset { let __start = RemoteShutter_CameraCapabilities.startCameraCapabilities(&fbb) RemoteShutter_CameraCapabilities.add(frontCamera: frontCamera, &fbb) @@ -1050,6 +1055,7 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { RemoteShutter_CameraCapabilities.add(activeDeviceId: activeDeviceId, &fbb) RemoteShutter_CameraCapabilities.add(supportsFocusPoint: supportsFocusPoint, &fbb) RemoteShutter_CameraCapabilities.add(supportsPreviewMode: supportsPreviewMode, &fbb) + RemoteShutter_CameraCapabilities.add(supportsMulticam: supportsMulticam, &fbb) return RemoteShutter_CameraCapabilities.endCameraCapabilities(&fbb, start: __start) } @@ -1061,6 +1067,7 @@ public struct RemoteShutter_CameraCapabilities: FlatBufferObject, Verifiable { try _v.visit(field: VTOFFSET.activeDeviceId.p, fieldName: "activeDeviceId", required: false, type: ForwardOffset.self) try _v.visit(field: VTOFFSET.supportsFocusPoint.p, fieldName: "supportsFocusPoint", required: false, type: Bool.self) try _v.visit(field: VTOFFSET.supportsPreviewMode.p, fieldName: "supportsPreviewMode", required: false, type: Bool.self) + try _v.visit(field: VTOFFSET.supportsMulticam.p, fieldName: "supportsMulticam", required: false, type: Bool.self) _v.finish() } } diff --git a/RemoteCam/RemoteCmdFlatBuffers.swift b/RemoteCam/RemoteCmdFlatBuffers.swift index 206e718a..a7d06001 100644 --- a/RemoteCam/RemoteCmdFlatBuffers.swift +++ b/RemoteCam/RemoteCmdFlatBuffers.swift @@ -418,7 +418,8 @@ private func encodeCapabilitiesEnvelope( cameraDevicesVectorOffset: devicesVector, activeDeviceIdOffset: activeIDOffset, supportsFocusPoint: c.supportsFocusPoint, - supportsPreviewMode: c.supportsPreviewMode) + supportsPreviewMode: c.supportsPreviewMode, + supportsMulticam: c.supportsMulticam) let stateOffset = RemoteShutter_CameraState.createCameraState( &fbb, @@ -1303,6 +1304,7 @@ extension RemoteCmd { activeDeviceID: activeDeviceID, supportsFocusPoint: caps?.supportsFocusPoint ?? false, supportsPreviewMode: caps?.supportsPreviewMode ?? false, + supportsMulticam: caps?.supportsMulticam ?? false, previewMode: state.map { fromFBPreviewMode($0.previewMode) } ?? .on, error: error ) diff --git a/RemoteCam/RemoteCmds.swift b/RemoteCam/RemoteCmds.swift index 4071071e..c8773367 100644 --- a/RemoteCam/RemoteCmds.swift +++ b/RemoteCam/RemoteCmds.swift @@ -410,6 +410,10 @@ public class RemoteCmd: Message, @unchecked Sendable { /// `RemoteCmd.SetCameraPreviewMode`. The monitor's standby gate reads /// this so it never sends the command to a peer that would misread it. public let supportsPreviewMode: Bool + /// True when this peer's build can join a multicam director session + /// (scheduled capture, stream profiles). A director must not send + /// multicam commands to a peer that doesn't advertise this. + public let supportsMulticam: Bool /// The camera's current local-preview mode, so the monitor can reflect /// it from the first capabilities exchange. public let previewMode: CameraPreviewMode @@ -426,6 +430,7 @@ public class RemoteCmd: Message, @unchecked Sendable { activeDeviceID: String? = nil, supportsFocusPoint: Bool = false, supportsPreviewMode: Bool = false, + supportsMulticam: Bool = false, previewMode: CameraPreviewMode = .on, error: Error?) { self.frontCamera = frontCamera @@ -441,6 +446,7 @@ public class RemoteCmd: Message, @unchecked Sendable { self.activeDeviceID = activeDeviceID self.supportsFocusPoint = supportsFocusPoint self.supportsPreviewMode = supportsPreviewMode + self.supportsMulticam = supportsMulticam self.previewMode = previewMode self.error = error super.init(sender: nil) diff --git a/RemoteCamTests/CaptureSyncMetadataTests.swift b/RemoteCamTests/CaptureSyncMetadataTests.swift new file mode 100644 index 00000000..b47ae2aa --- /dev/null +++ b/RemoteCamTests/CaptureSyncMetadataTests.swift @@ -0,0 +1,73 @@ +// +// CaptureSyncMetadataTests.swift +// RemoteShutterTests +// +// Created by Dario Lencina on 2026. +// Copyright © 2026 Security Union. All rights reserved. +// + +import AVFoundation +import XCTest +@testable import RemoteShutter + +final class CaptureSyncMetadataTests: XCTestCase { + + private let sample = CaptureSyncMetadata( + sessionID: "6BB65B12-30A4-4A5C-9F41-000000000001", + captureID: "D0E1F2A3-1111-2222-3333-000000000002", + cameraIndex: 3, + anchorMillis: 1_754_800_000_123, + clockOffsetMillis: -42, + roundTripMillis: 11 + ) + + func testFilenamePrefixGroupsBySessionCaptureAndCamera() { + XCTAssertEqual(sample.filenamePrefix, "RS_6bb65b12_d0e1f2a3_cam3") + } + + func testJSONRoundTrip() { + guard let json = sample.jsonString() else { + return XCTFail("expected JSON encoding to succeed") + } + XCTAssertEqual(CaptureSyncMetadata.fromJSONString(json), sample) + } + + func testJSONIsDeterministic() { + XCTAssertEqual(sample.jsonString(), sample.jsonString()) + } + + func testQuickTimeItemsCarryAnchorAndIDs() { + let items = sample.quickTimeMetadataItems() + XCTAssertEqual(items.count, 4) + + func value(for key: String) -> Any? { + items.first { ($0.key as? String) == key }?.value + } + XCTAssertEqual( + (value(for: CaptureSyncMetadata.QuickTimeKey.anchor) as? NSNumber)?.uint64Value, + sample.anchorMillis) + XCTAssertEqual( + value(for: CaptureSyncMetadata.QuickTimeKey.capture) as? String, + sample.captureID) + XCTAssertEqual( + value(for: CaptureSyncMetadata.QuickTimeKey.session) as? String, + sample.sessionID) + XCTAssertEqual( + (value(for: CaptureSyncMetadata.QuickTimeKey.offset) as? NSNumber)?.int64Value, + sample.clockOffsetMillis) + for item in items { + XCTAssertEqual(item.keySpace, .quickTimeMetadata) + XCTAssertNotNil(item.identifier) + } + } + + /// The wire capability appended for multicam must default to false for + /// legacy peers (absent field) — PR0 ships it inert. + func testCapabilitiesDefaultToNoMulticam() { + let resp = RemoteCmd.CameraCapabilitiesResp( + frontCamera: nil, backCamera: nil, + currentCamera: .back, currentLens: .wideAngle, + currentZoom: 1.0, error: nil) + XCTAssertFalse(resp.supportsMulticam) + } +} diff --git a/RemoteCamTests/RemoteCmdSerializationTests.swift b/RemoteCamTests/RemoteCmdSerializationTests.swift index a045eccf..72153162 100644 --- a/RemoteCamTests/RemoteCmdSerializationTests.swift +++ b/RemoteCamTests/RemoteCmdSerializationTests.swift @@ -1182,4 +1182,20 @@ extension RemoteCmdSerializationTests { XCTAssertFalse(result.supportsPreviewMode) XCTAssertEqual(result.previewMode, .on) } + + /// The multicam capability survives the wire, and a peer that predates it + /// (absent field) decodes as not-multicam-capable. + func testCapabilitiesCarryMulticamSupport() { + let caps = RemoteCmd.CameraCapabilitiesResp( + frontCamera: nil, backCamera: nil, + currentCamera: .back, currentLens: .wideAngle, currentZoom: 1.0, + supportsMulticam: true, error: nil) + XCTAssertTrue(roundTrip(caps).supportsMulticam) + + let legacy = RemoteCmd.CameraCapabilitiesResp( + frontCamera: nil, backCamera: nil, + currentCamera: .back, currentLens: .wideAngle, currentZoom: 1.0, + error: nil) + XCTAssertFalse(roundTrip(legacy).supportsMulticam) + } } diff --git a/RemoteShutter.xcodeproj/project.pbxproj b/RemoteShutter.xcodeproj/project.pbxproj index 223ad010..6cbb744b 100644 --- a/RemoteShutter.xcodeproj/project.pbxproj +++ b/RemoteShutter.xcodeproj/project.pbxproj @@ -201,6 +201,8 @@ FADEC0DE0002000000000002 /* PeerLinkStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEC0DE0002000000000001 /* PeerLinkStatus.swift */; }; FC0CF5A101FE65A9800F0B238 /* FocusPointMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC0CF5A201FE65A9800F0B238 /* FocusPointMapping.swift */; }; CAFEBABE0120000000000001 /* MonitorChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFEBABE0120000000000002 /* MonitorChrome.swift */; }; + CAFEBABE0130000000000001 /* CaptureSyncMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFEBABE0130000000000002 /* CaptureSyncMetadata.swift */; }; + CAFEBABE0131000000000002 /* CaptureSyncMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFEBABE0131000000000001 /* CaptureSyncMetadataTests.swift */; }; FEEDFACE0000000000000001 /* StormoLoopbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDFACE0000000000000002 /* StormoLoopbackTests.swift */; }; /* End PBXBuildFile section */ @@ -442,6 +444,8 @@ FADEC0DE0002000000000001 /* PeerLinkStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerLinkStatus.swift; sourceTree = ""; }; FC0CF5A201FE65A9800F0B238 /* FocusPointMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusPointMapping.swift; sourceTree = ""; }; CAFEBABE0120000000000002 /* MonitorChrome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonitorChrome.swift; sourceTree = ""; }; + CAFEBABE0130000000000002 /* CaptureSyncMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureSyncMetadata.swift; sourceTree = ""; }; + CAFEBABE0131000000000001 /* CaptureSyncMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSyncMetadataTests.swift; sourceTree = ""; }; FCB0F6BA2086BF9BB4242D66 /* Pods_RemoteShutterWatch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RemoteShutterWatch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FEEDFACE0000000000000002 /* StormoLoopbackTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StormoLoopbackTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -591,6 +595,7 @@ CAFEBABE0001000000000001 /* CropRectTests.swift */, CAFEBABE00F0000000000001 /* FocusPointMappingTests.swift */, CAFEBABE0121000000000001 /* MonitorChromeTests.swift */, + CAFEBABE0131000000000001 /* CaptureSyncMetadataTests.swift */, CAFEBABE0002000000000001 /* WatchCaptureCountdownTests.swift */, CAFEBABE0003000000000001 /* WatchSerializationTests.swift */, CAFEBABE0006000000000001 /* WatchPreviewStreamerTests.swift */, @@ -683,6 +688,7 @@ 0684A2D01BE65A9800F0B238 /* OrientationUtils.swift */, FC0CF5A201FE65A9800F0B238 /* FocusPointMapping.swift */, CAFEBABE0120000000000002 /* MonitorChrome.swift */, + CAFEBABE0130000000000002 /* CaptureSyncMetadata.swift */, 060B1E141BE7079800077BCC /* Helpers */, 06E965202535199400E5A8B3 /* MediaProcessors.swift */, 06E9652625351E3F00E5A8B3 /* SwiftConstants.swift */, @@ -1175,6 +1181,7 @@ 0684A2D11BE65A9800F0B238 /* OrientationUtils.swift in Sources */, FC0CF5A101FE65A9800F0B238 /* FocusPointMapping.swift in Sources */, CAFEBABE0120000000000001 /* MonitorChrome.swift in Sources */, + CAFEBABE0130000000000001 /* CaptureSyncMetadata.swift in Sources */, 06BB79B82E374D410094E085 /* FeatureFlags.swift in Sources */, 0692844F1BE5C0E600AF4678 /* MultipeerMessages.swift in Sources */, 069284531BE5C0E600AF4678 /* RemoteCamStateNames.swift in Sources */, @@ -1247,6 +1254,7 @@ CAFEBABE0001000000000002 /* CropRectTests.swift in Sources */, CAFEBABE00F0000000000002 /* FocusPointMappingTests.swift in Sources */, CAFEBABE0121000000000002 /* MonitorChromeTests.swift in Sources */, + CAFEBABE0131000000000002 /* CaptureSyncMetadataTests.swift in Sources */, CAFEBABE0002000000000002 /* WatchCaptureCountdownTests.swift in Sources */, CAFEBABE0003000000000002 /* WatchSerializationTests.swift in Sources */, CAFEBABE0006000000000002 /* WatchPreviewStreamerTests.swift in Sources */,