From 6b2fb4cff8803f6913d5dc9116d0950e0087478e Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Fri, 18 Oct 2019 18:38:01 +0300 Subject: [PATCH 01/39] Update version to 0.11.0 --- CHANGELOG.md | 10 +++++++++- Examples/Cartfile | 4 ++-- MapboxVision.podspec | 2 +- MapboxVision/Info.plist | 2 +- MapboxVisionAR.podspec | 2 +- MapboxVisionAR/Info.plist | 2 +- MapboxVisionAll.podspec | 2 +- MapboxVisionSafety.podspec | 2 +- MapboxVisionSafety/Info.plist | 2 +- MapboxVisionTests/Info.plist | 2 +- 10 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2934771..0129452a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.11.0 - Unreleased + +### Vision + +### AR + +### Safety + ## 0.10.0 ### Vision @@ -17,7 +25,7 @@ - Renamed property `frameVisualizationMode` to `visualizationMode` in `VisionPresentationViewController` - Updated detection models, added construction cone class, improved metrics -## AR +### AR - Changed `VisionARViewController` to show camera frames as long as `VisionARManager` exists ## 0.9.0 diff --git a/Examples/Cartfile b/Examples/Cartfile index e10b14df..c0051be4 100644 --- a/Examples/Cartfile +++ b/Examples/Cartfile @@ -1,4 +1,4 @@ -# binary "https://api.mapbox.com/downloads/v1/vision/ios/mapbox-vision-native-all.carthage?access_token=" ~> 0.10.0 -# github "mapbox/mapbox-vision-ios" ~> 0.10.0 +# binary "https://api.mapbox.com/downloads/v1/vision/ios/mapbox-vision-native-all.carthage?access_token=" ~> 0.11.0 +# github "mapbox/mapbox-vision-ios" ~> 0.11.0 github "mapbox/MapboxDirections.swift" ~> 0.28.0 diff --git a/MapboxVision.podspec b/MapboxVision.podspec index 1c75bd1a..f8f96498 100644 --- a/MapboxVision.podspec +++ b/MapboxVision.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MapboxVision" - s.version = "0.10.0" + s.version = "0.11.0" s.summary = "ML empowered vision framework" s.homepage = 'https://www.mapbox.com/vision/' diff --git a/MapboxVision/Info.plist b/MapboxVision/Info.plist index 872bbbc6..fc1b993a 100644 --- a/MapboxVision/Info.plist +++ b/MapboxVision/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.10.0 + 0.11.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/MapboxVisionAR.podspec b/MapboxVisionAR.podspec index 96c05e78..8c742643 100644 --- a/MapboxVisionAR.podspec +++ b/MapboxVisionAR.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MapboxVisionAR" - s.version = "0.10.0" + s.version = "0.11.0" s.summary = "Easy to use AR Navigation" s.homepage = 'https://www.mapbox.com/vision/' diff --git a/MapboxVisionAR/Info.plist b/MapboxVisionAR/Info.plist index 872bbbc6..fc1b993a 100644 --- a/MapboxVisionAR/Info.plist +++ b/MapboxVisionAR/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.10.0 + 0.11.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/MapboxVisionAll.podspec b/MapboxVisionAll.podspec index f79610b6..3274d7db 100644 --- a/MapboxVisionAll.podspec +++ b/MapboxVisionAll.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MapboxVisionAll" - s.version = "0.10.0" + s.version = "0.11.0" s.summary = "ML empowered vision framework" s.homepage = 'https://www.mapbox.com/vision/' diff --git a/MapboxVisionSafety.podspec b/MapboxVisionSafety.podspec index 2c6a346f..64f80e23 100644 --- a/MapboxVisionSafety.podspec +++ b/MapboxVisionSafety.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MapboxVisionSafety" - s.version = "0.10.0" + s.version = "0.11.0" s.summary = "Safety features on top of Vision" s.homepage = 'https://www.mapbox.com/vision/' diff --git a/MapboxVisionSafety/Info.plist b/MapboxVisionSafety/Info.plist index a5743c42..683222bb 100644 --- a/MapboxVisionSafety/Info.plist +++ b/MapboxVisionSafety/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.10.0 + 0.11.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/MapboxVisionTests/Info.plist b/MapboxVisionTests/Info.plist index 94c5a772..57d66c3f 100644 --- a/MapboxVisionTests/Info.plist +++ b/MapboxVisionTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 0.10.0 + 0.11.0 CFBundleVersion 1 From 62353d0e79305196b49b917e98ab8efcea729105 Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Mon, 21 Oct 2019 14:53:17 +0300 Subject: [PATCH 02/39] Add doc comment to Examples --- .../VisionExamples/Examples/ARNavigationViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/VisionExamples/Examples/ARNavigationViewController.swift b/Examples/VisionExamples/Examples/ARNavigationViewController.swift index 15ea36ca..251eb0dc 100644 --- a/Examples/VisionExamples/Examples/ARNavigationViewController.swift +++ b/Examples/VisionExamples/Examples/ARNavigationViewController.swift @@ -28,7 +28,7 @@ class ARNavigationViewController: UIViewController { visionManager = VisionManager.create(videoSource: videoSource) // create VisionARManager and register as its delegate to receive AR related events visionARManager = VisionARManager.create(visionManager: visionManager) - + /// configure ar view to display AR navigation visionARViewController.set(arManager: visionARManager) let origin = CLLocationCoordinate2D() From 7987ffd20d52ad6f80c56816c1254978b93a0da9 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 30 Oct 2019 18:38:47 +0300 Subject: [PATCH 03/39] Support Germany in sync region --- MapboxVision/Helpers/Country+SyncRegion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapboxVision/Helpers/Country+SyncRegion.swift b/MapboxVision/Helpers/Country+SyncRegion.swift index a9360fcf..df12db26 100644 --- a/MapboxVision/Helpers/Country+SyncRegion.swift +++ b/MapboxVision/Helpers/Country+SyncRegion.swift @@ -3,7 +3,7 @@ import Foundation extension Country { var syncRegion: SyncRegion? { switch self { - case .USA, .UK, .other: + case .USA, .UK, .germany, .other: return .other case .china: return .china From ca20201a984dc8b4a64cc9fff61be6f33cda5a8a Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Tue, 22 Oct 2019 13:17:04 +0300 Subject: [PATCH 04/39] Review fixes --- .../VisionExamples/Examples/ARNavigationViewController.swift | 4 ++-- .../Examples/ExternalCameraViewController.swift | 2 +- .../VisionExamples/Examples/OverSpeedingViewController.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/VisionExamples/Examples/ARNavigationViewController.swift b/Examples/VisionExamples/Examples/ARNavigationViewController.swift index 251eb0dc..a70f4c52 100644 --- a/Examples/VisionExamples/Examples/ARNavigationViewController.swift +++ b/Examples/VisionExamples/Examples/ARNavigationViewController.swift @@ -26,9 +26,9 @@ class ARNavigationViewController: UIViewController { // create VisionManager with video source visionManager = VisionManager.create(videoSource: videoSource) - // create VisionARManager and register as its delegate to receive AR related events + // create VisionARManager visionARManager = VisionARManager.create(visionManager: visionManager) - /// configure ar view to display AR navigation + // configure AR view to display AR navigation visionARViewController.set(arManager: visionARManager) let origin = CLLocationCoordinate2D() diff --git a/Examples/VisionExamples/Examples/ExternalCameraViewController.swift b/Examples/VisionExamples/Examples/ExternalCameraViewController.swift index 6384dd91..ca373804 100644 --- a/Examples/VisionExamples/Examples/ExternalCameraViewController.swift +++ b/Examples/VisionExamples/Examples/ExternalCameraViewController.swift @@ -82,6 +82,7 @@ class ExternalCameraViewController: UIViewController, VisionManagerDelegate { // create VisionManager with a custom video source visionManager = VisionManager.create(videoSource: fileVideoSource) + visionManager.delegate = self // configure view to display sample buffers from video source visionViewController.set(visionManager: visionManager) @@ -90,7 +91,6 @@ class ExternalCameraViewController: UIViewController, VisionManagerDelegate { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - visionManager.delegate = self visionManager.start() fileVideoSource.start() } diff --git a/Examples/VisionExamples/Examples/OverSpeedingViewController.swift b/Examples/VisionExamples/Examples/OverSpeedingViewController.swift index 17069b50..109523ee 100644 --- a/Examples/VisionExamples/Examples/OverSpeedingViewController.swift +++ b/Examples/VisionExamples/Examples/OverSpeedingViewController.swift @@ -29,6 +29,7 @@ class OverSpeedingViewController: UIViewController { // create VisionManager with video source visionManager = VisionManager.create(videoSource: videoSource) + visionManager.delegate = self // create VisionSafetyManager and register as its delegate to receive safety related events visionSafetyManager = VisionSafetyManager.create(visionManager: visionManager) visionSafetyManager.delegate = self @@ -40,7 +41,6 @@ class OverSpeedingViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - visionManager.delegate = self visionManager.start() videoSource.start() } From 179af1ef4057f8d3d969b8557e523f836c6c7db8 Mon Sep 17 00:00:00 2001 From: Vlad Arbatov Date: Tue, 24 Sep 2019 15:44:26 +0300 Subject: [PATCH 05/39] Added github/stale.yml Added initial stalebot config. All issues that are 45 days old and not closed and not with `nostale` label will be labeled as `staled` and commented on (but not closed, maybe changed later). More info on stalebot https://github.com/probot/stale. `nostale` label applied to all issued apart from `User feedback`. --- .github/stale.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..673b57e0 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,15 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 45 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 5000 +# Issues with these labels will never be considered stale +exemptLabels: + - nostale +# Label to use when marking an issue as stale +staleLabel: staled +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 1b8a087a94ba715ab6dade68502051ef7604dd44 Mon Sep 17 00:00:00 2001 From: Vlad Arbatov Date: Tue, 24 Sep 2019 15:58:40 +0300 Subject: [PATCH 06/39] Update stale.yml --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 673b57e0..098861c3 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Number of days of inactivity before an issue becomes stale daysUntilStale: 45 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 5000 +daysUntilClose: false # Issues with these labels will never be considered stale exemptLabels: - nostale From 453c14549a26b324d5555a31ac07eb51ed84b53e Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 6 Nov 2019 12:49:46 +0300 Subject: [PATCH 07/39] Add iPhone 11 and family to the list of high-performant devices --- MapboxVision/Helpers/DeviceChecker.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapboxVision/Helpers/DeviceChecker.swift b/MapboxVision/Helpers/DeviceChecker.swift index bea5b579..2e2e344f 100644 --- a/MapboxVision/Helpers/DeviceChecker.swift +++ b/MapboxVision/Helpers/DeviceChecker.swift @@ -9,7 +9,7 @@ private enum DeviceModel { enum MajorNumber { static let minIphoneVersionWithHighPerformance = 10 // "10" corresponds to 8/8 Plus/X. - static let maxIphoneVersionWithHighPerformance = 11 // "11" corresponds to XR/XS/Xs Max. + static let maxIphoneVersionWithHighPerformance = 12 // "12" corresponds to 11/11 Pro/11 Pro Max. } } From cc7010e904c73321dda1ebbdeb1ef273175103b6 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 6 Nov 2019 13:29:07 +0300 Subject: [PATCH 08/39] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0129452a..fe8d80e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ ### Safety +## 0.10.1 + +### Vision +- Increased allowed performance of ML models on iPhone 11 family + +### AR +- Fixed displaying AR in conditions of low GPS accuracy + ## 0.10.0 ### Vision From c5c73e8a3c5e4aaa5d1807831cbe0cc294e49ff5 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Mon, 4 Nov 2019 22:16:02 +0300 Subject: [PATCH 09/39] Simplification of dependency graph --- MapboxVision/Core/Platform.swift | 41 ++++- .../Services/Recording/VideoRecorder.swift | 16 ++ .../VisionManager/ManagerDependencies.swift | 65 ++++--- .../VisionManager/VisionManager.swift | 170 +++++++++--------- 4 files changed, 167 insertions(+), 125 deletions(-) diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift index 64eff66a..ae50c88f 100644 --- a/MapboxVision/Core/Platform.swift +++ b/MapboxVision/Core/Platform.swift @@ -1,10 +1,11 @@ import Foundation import MapboxVisionNative -final class Platform: NSObject, PlatformInterface { +final class Platform: NSObject { struct Dependencies { - let recordCoordinator: RecordCoordinator? + let recorder: VideoRecorder? let eventsManager: EventsManager + let archiver: Archiver? } private let dependencies: Dependencies @@ -12,12 +13,10 @@ final class Platform: NSObject, PlatformInterface { init(dependencies: Dependencies) { self.dependencies = dependencies } +} - func makeVideoClip(_ startTime: Float, end endTime: Float) { - dependencies.recordCoordinator?.makeClip(from: startTime, to: endTime) - } - - func sendTelemetry(_ name: String, entries: [TelemetryEntry]) { +extension Platform: PlatformInterface { + func sendTelemetry(name: String, entries: [TelemetryEntry]) { let entries = Dictionary(entries.map { ($0.key, $0.value) }) { first, _ in assertionFailure("Duplicated key in telemetry entries.") return first @@ -26,7 +25,31 @@ final class Platform: NSObject, PlatformInterface { dependencies.eventsManager.sendEvent(name: name, entries: entries) } - func save(image: Image, path: String) { - dependencies.recordCoordinator?.saveImage(image: image, path: path) + func sendTelemetryFile(path: String, callback: @escaping SuccessCallback) { + dependencies.eventsManager.upload(file: URL(fileURLWithPath: path), + toFolder: "") { error in callback(error == nil) } + } + + func startVideoRecording(filePath: String) { + dependencies.recorder?.startRecording(to: filePath, settings: .lowQuality) + } + + func stopVideoRecording() { + dependencies.recorder?.stopRecording(completion: nil) + } + + func makeVideoClips(inputFilePath: String, outputDirectoryPath: String, clips: [VideoClip], callback: @escaping SuccessCallback) {} + + func archiveFiles(filePaths: [String], archivePath: String, callback: @escaping SuccessCallback) { + do { + try dependencies.archiver?.archive(filePaths.map(URL.init(fileURLWithPath:)), + destination: URL(fileURLWithPath: archivePath)) + } catch { + assertionFailure("ERROR: archiving failed with error: \(error.localizedDescription)") + callback(false) + return + } + + callback(true) } } diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index 063ad969..d7f1635a 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -18,6 +18,12 @@ private extension VideoSettings { } } +protocol FrameRecordable { + func startRecording(filePath: String, settings: VideoSettings) + func stopRecording(completion: (() -> Void)?) + func handle(frame: CMSampleBuffer) +} + final class VideoRecorder { private var currentAssetWriterInput: AVAssetWriterInput? private var currentAssetWriter: AVAssetWriter? @@ -125,6 +131,16 @@ final class VideoRecorder { } } +extension VideoRecorder: FrameRecordable { + func startRecording(filePath: String, settings: VideoSettings) { + startRecording(to: filePath, settings: settings) + } + + func handle(frame: CMSampleBuffer) { + handleFrame(frame) { _ in } + } +} + private extension CMTime { func millis(since: CMTime) -> Float64 { let passed = CMTimeSubtract(self, since) diff --git a/MapboxVision/VisionManager/ManagerDependencies.swift b/MapboxVision/VisionManager/ManagerDependencies.swift index 12a3a2e0..a4aa19bd 100644 --- a/MapboxVision/VisionManager/ManagerDependencies.swift +++ b/MapboxVision/VisionManager/ManagerDependencies.swift @@ -9,51 +9,49 @@ struct BaseDependencies { struct VisionDependencies { let native: VisionManagerNativeProtocol - let synchronizer: Synchronizable - let recorder: SessionRecorderProtocol +// let synchronizer: Synchronizable +// let recorder: SessionRecorderProtocol + let recorder: FrameRecordable let dataProvider: DataProvider let deviceInfo: DeviceInfoProvidable static func `default`() -> VisionDependencies { - guard let reachability = Reachability() else { - fatalError("Reachability failed to initialize") - } +// guard let reachability = Reachability() else { +// fatalError("Reachability failed to initialize") +// } let eventsManager = EventsManager() let deviceInfo = DeviceInfoProvider() let recordArchiver = RecordArchiver() - let recordSyncDependencies = RecordSynchronizer.Dependencies( - networkClient: eventsManager, - deviceInfo: deviceInfo, - archiver: recordArchiver, - fileManager: FileManager.default - ) - let recordSynchronizer = RecordSynchronizer(recordSyncDependencies) - - let syncDependencies = ManagedSynchronizer.Dependencies( - base: recordSynchronizer, - reachability: reachability - ) - let synchronizer = ManagedSynchronizer(dependencies: syncDependencies) - - let recordCoordinator = RecordCoordinator() + + let recorder = VideoRecorder() +// let recordSyncDependencies = RecordSynchronizer.Dependencies( +// networkClient: eventsManager, +// deviceInfo: deviceInfo, +// archiver: recordArchiver, +// fileManager: FileManager.default +// ) +// let recordSynchronizer = RecordSynchronizer(recordSyncDependencies) + +// let recordCoordinator = RecordCoordinator() let platform = Platform(dependencies: Platform.Dependencies( - recordCoordinator: recordCoordinator, - eventsManager: eventsManager + recorder: recorder, + eventsManager: eventsManager, + archiver: recordArchiver )) let native = VisionManagerNative.create(withPlatform: platform) - let recorder = SessionRecorder(dependencies: SessionRecorder.Dependencies( - recorder: recordCoordinator, - sessionManager: SessionManager(), - videoSettings: visionVideoSettings, - getSeconds: native.getSeconds, - startSavingSession: native.startSavingSession, - stopSavingSession: native.stopSavingSession - )) +// let recorder = SessionRecorder(dependencies: SessionRecorder.Dependencies( +// recorder: recordCoordinator, +// sessionManager: SessionManager(), +// videoSettings: visionVideoSettings, +// getSeconds: native.getSeconds, +// startSavingSession: native.startSavingSession, +// stopSavingSession: native.stopSavingSession +// )) let dataProvider = RealtimeDataProvider(dependencies: RealtimeDataProvider.Dependencies( native: native, @@ -62,7 +60,7 @@ struct VisionDependencies { )) return VisionDependencies(native: native, - synchronizer: synchronizer, +// synchronizer: synchronizer, recorder: recorder, dataProvider: dataProvider, deviceInfo: deviceInfo) @@ -82,8 +80,9 @@ struct ReplayDependencies { let eventsManager = EventsManager() let platform = Platform(dependencies: Platform.Dependencies( - recordCoordinator: nil, - eventsManager: eventsManager + recorder: nil, + eventsManager: eventsManager, + archiver: RecordArchiver() )) let native = VisionReplayManagerNative.create(withPlatform: platform, recordPath: recordPath) diff --git a/MapboxVision/VisionManager/VisionManager.swift b/MapboxVision/VisionManager/VisionManager.swift index 4c37f432..010e03ae 100644 --- a/MapboxVision/VisionManager/VisionManager.swift +++ b/MapboxVision/VisionManager/VisionManager.swift @@ -107,8 +107,10 @@ public final class VisionManager: BaseVisionManager { guard case .started = state else { throw VisionManagerError.startRecordingBeforeStart } - dependencies.recorder.stop() - dependencies.recorder.start(mode: .external(path: path)) +// dependencies.native.startRecordint(to: path) + +// dependencies.recorder.stop() +// dependencies.recorder.start(mode: .external(path: path)) } /** @@ -123,8 +125,10 @@ public final class VisionManager: BaseVisionManager { assertionFailure("VisionManager should be started and recording") return } - dependencies.recorder.stop() - dependencies.recorder.start(mode: .internal) + // dependencies.native.stopRecording() + +// dependencies.recorder.stop() +// dependencies.recorder.start(mode: .internal) } /** @@ -188,7 +192,7 @@ public final class VisionManager: BaseVisionManager { state = .initialized(videoSource: videoSource) - dependencies.recorder.delegate = self +// dependencies.recorder.delegate = self dependencies.native.videoSource = VideoSourceObserverProxy(withVideoSource: videoSource) } @@ -196,9 +200,9 @@ public final class VisionManager: BaseVisionManager { destroy() } - private var isSyncAllowed: Bool { - return currentCountry.syncRegion != nil - } +// private var isSyncAllowed: Bool { +// return currentCountry.syncRegion != nil +// } private func startVideoStream() { guard case let .started(videoSource) = state else { return } @@ -215,7 +219,7 @@ public final class VisionManager: BaseVisionManager { startVideoStream() dependencies.native.start() - dependencies.recorder.start(mode: .internal) +// dependencies.recorder.start(mode: .internal) } private func pause() { @@ -223,46 +227,46 @@ public final class VisionManager: BaseVisionManager { stopVideoStream() dependencies.native.stop() - dependencies.recorder.stop() - } - - private func configureRecording(oldCountry: Country, newCountry: Country) { - guard - state.isStarted, - dependencies.recorder.isInternal, - let oldRegion = oldCountry.syncRegion, - oldRegion != newCountry.syncRegion - else { return } - - if let path = currentRecordingPath { - recordingToCountryCache[path] = oldCountry - } - dependencies.recorder.stop() - dependencies.recorder.start(mode: .internal) +// dependencies.recorder.stop() } - private func configureSync(oldCountry: Country, newCountry: Country) { - if newCountry.syncRegion == nil { - dependencies.synchronizer.stopSync() - return - } - - guard - let newRegion = newCountry.syncRegion, - oldCountry.syncRegion != newRegion - else { return } - - let dataSource = SyncRecordDataSource(region: newRegion) - dependencies.synchronizer.stopSync() - dependencies.synchronizer.set(dataSource: dataSource, baseURL: newRegion.baseURL) - dependencies.synchronizer.sync() - } - - private func trySync() { - if isSyncAllowed { - dependencies.synchronizer.sync() - } - } +// private func configureRecording(oldCountry: Country, newCountry: Country) { +// guard +// state.isStarted, +// dependencies.recorder.isInternal, +// let oldRegion = oldCountry.syncRegion, +// oldRegion != newCountry.syncRegion +// else { return } +// +// if let path = currentRecordingPath { +// recordingToCountryCache[path] = oldCountry +// } +// dependencies.recorder.stop() +// dependencies.recorder.start(mode: .internal) +// } + +// private func configureSync(oldCountry: Country, newCountry: Country) { +// if newCountry.syncRegion == nil { +// dependencies.synchronizer.stopSync() +// return +// } +// +// guard +// let newRegion = newCountry.syncRegion, +// oldCountry.syncRegion != newRegion +// else { return } +// +// let dataSource = SyncRecordDataSource(region: newRegion) +// dependencies.synchronizer.stopSync() +// dependencies.synchronizer.set(dataSource: dataSource, baseURL: newRegion.baseURL) +// dependencies.synchronizer.sync() +// } + +// private func trySync() { +// if isSyncAllowed { +// dependencies.synchronizer.sync() +// } +// } override func prepareForBackground() { guard state.isStarted else { return } @@ -276,15 +280,15 @@ public final class VisionManager: BaseVisionManager { resume() } - override public func onCountryUpdated(_ country: Country) { - let oldCountry = currentCountry - currentCountry = country - - configureRecording(oldCountry: oldCountry, newCountry: country) - configureSync(oldCountry: oldCountry, newCountry: country) - - super.onCountryUpdated(country) - } +// override public func onCountryUpdated(_ country: Country) { +// let oldCountry = currentCountry +// currentCountry = country +// +// configureRecording(oldCountry: oldCountry, newCountry: country) +// configureSync(oldCountry: oldCountry, newCountry: country) +// +// super.onCountryUpdated(country) +// } } /// :nodoc: @@ -299,7 +303,7 @@ extension VisionManager: VideoSourceObserver { guard state.isStarted else { return } - dependencies.recorder.handleFrame(videoSample.buffer) + dependencies.recorder.handle(frame: videoSample.buffer) dependencies.native.sensors.setImage(pixelBuffer) } @@ -308,29 +312,29 @@ extension VisionManager: VideoSourceObserver { } } -extension VisionManager: RecordCoordinatorDelegate { - func recordingStarted(path: String) { - currentRecordingPath = path.lastPathComponent - } - - func recordingStopped(recordingPath: RecordingPath) { - currentRecordingPath = nil - - let country = recordingToCountryCache.removeValue(forKey: recordingPath.recordingPath.lastPathComponent) - ?? currentCountry - - try? handle(recordingPath: recordingPath, country: country) - trySync() - } - - private func handle(recordingPath: RecordingPath, country: Country) throws { - guard recordingPath.basePath != .custom else { return } - - guard let syncRegion = country.syncRegion else { - try recordingPath.delete() - return - } - - try recordingPath.move(to: .recordings(syncRegion)) - } -} +//extension VisionManager: RecordCoordinatorDelegate { +// func recordingStarted(path: String) { +// currentRecordingPath = path.lastPathComponent +// } +// +// func recordingStopped(recordingPath: RecordingPath) { +// currentRecordingPath = nil +// +// let country = recordingToCountryCache.removeValue(forKey: recordingPath.recordingPath.lastPathComponent) +// ?? currentCountry +// +// try? handle(recordingPath: recordingPath, country: country) +//// trySync() +// } +// +// private func handle(recordingPath: RecordingPath, country: Country) throws { +// guard recordingPath.basePath != .custom else { return } +// +// guard let syncRegion = country.syncRegion else { +// try recordingPath.delete() +// return +// } +// +// try recordingPath.move(to: .recordings(syncRegion)) +// } +//} From 428587b24e59c22197ad8bfcfbc652aff8ecf387 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Tue, 5 Nov 2019 21:01:27 +0300 Subject: [PATCH 10/39] Make archiving asynchronous --- MapboxVision/Core/Platform.swift | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift index ae50c88f..cdfbd168 100644 --- a/MapboxVision/Core/Platform.swift +++ b/MapboxVision/Core/Platform.swift @@ -41,15 +41,17 @@ extension Platform: PlatformInterface { func makeVideoClips(inputFilePath: String, outputDirectoryPath: String, clips: [VideoClip], callback: @escaping SuccessCallback) {} func archiveFiles(filePaths: [String], archivePath: String, callback: @escaping SuccessCallback) { - do { - try dependencies.archiver?.archive(filePaths.map(URL.init(fileURLWithPath:)), - destination: URL(fileURLWithPath: archivePath)) - } catch { - assertionFailure("ERROR: archiving failed with error: \(error.localizedDescription)") - callback(false) - return + DispatchQueue.global(qos: .utility).async { + do { + try self.dependencies.archiver?.archive(filePaths.map(URL.init(fileURLWithPath:)), + destination: URL(fileURLWithPath: archivePath)) + } catch { + assertionFailure("ERROR: archiving failed with error: \(error.localizedDescription)") + callback(false) + return + } + + callback(true) } - - callback(true) } } From 34c577cb8dc41b0696c34d4a38bd80726e62db14 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Tue, 5 Nov 2019 22:52:31 +0300 Subject: [PATCH 11/39] Adjust telemetry related methods --- MapboxVision/Core/Platform.swift | 11 ++++++++--- MapboxVision/Services/EventsManager.swift | 5 ++++- MapboxVision/Services/Network/NetworkClient.swift | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift index cdfbd168..54bc07df 100644 --- a/MapboxVision/Core/Platform.swift +++ b/MapboxVision/Core/Platform.swift @@ -1,6 +1,8 @@ import Foundation import MapboxVisionNative +typealias TelemetryFileMetadata = [String: String] + final class Platform: NSObject { struct Dependencies { let recorder: VideoRecorder? @@ -16,6 +18,10 @@ final class Platform: NSObject { } extension Platform: PlatformInterface { + func setSyncUrl(_ url: String) { + dependencies.eventsManager.set(baseURL: URL(string: url)) + } + func sendTelemetry(name: String, entries: [TelemetryEntry]) { let entries = Dictionary(entries.map { ($0.key, $0.value) }) { first, _ in assertionFailure("Duplicated key in telemetry entries.") @@ -25,9 +31,8 @@ extension Platform: PlatformInterface { dependencies.eventsManager.sendEvent(name: name, entries: entries) } - func sendTelemetryFile(path: String, callback: @escaping SuccessCallback) { - dependencies.eventsManager.upload(file: URL(fileURLWithPath: path), - toFolder: "") { error in callback(error == nil) } + func sendTelemetryFile(path: String, metadata: TelemetryFileMetadata, callback: @escaping SuccessCallback) { + dependencies.eventsManager.upload(file: path, metadata: metadata) { error in callback(error == nil) } } func startVideoRecording(filePath: String) { diff --git a/MapboxVision/Services/EventsManager.swift b/MapboxVision/Services/EventsManager.swift index 2a662f66..82eb1b45 100644 --- a/MapboxVision/Services/EventsManager.swift +++ b/MapboxVision/Services/EventsManager.swift @@ -38,7 +38,6 @@ final class EventsManager { ) manager.sendTurnstileEvent() manager.isMetricsEnabled = true - manager.isDebugLoggingEnabled = true } func sendEvent(name: String, entries: [String: Any]) { @@ -51,6 +50,10 @@ extension EventsManager: NetworkClient { manager.baseURL = baseURL } + func upload(file: String, metadata: TelemetryFileMetadata, completion: @escaping (Error?) -> Void) { + manager.postMetadata([metadata], filePaths: [file], completionHandler: completion) + } + func upload(file: URL, toFolder folderName: String, completion: @escaping (Error?) -> Void) { let contentType: String switch file.pathExtension { diff --git a/MapboxVision/Services/Network/NetworkClient.swift b/MapboxVision/Services/Network/NetworkClient.swift index d77f8d81..6ad5c61f 100644 --- a/MapboxVision/Services/Network/NetworkClient.swift +++ b/MapboxVision/Services/Network/NetworkClient.swift @@ -1,7 +1,9 @@ import Foundation +import MapboxVisionNative protocol NetworkClient { func set(baseURL: URL?) func upload(file: URL, toFolder folderName: String, completion: @escaping (Error?) -> Void) + func upload(file: String, metadata: TelemetryFileMetadata, completion: @escaping (Error?) -> Void) func cancel() } From 3fee2e5bb0b87c0e972a56d3f6e5738341062752 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Tue, 12 Nov 2019 11:37:46 +0100 Subject: [PATCH 12/39] Utilize recording methods --- MapboxVision/VisionManager/VisionManager.swift | 10 ++-------- .../VisionManager/VisionManagerNativeProtocol.swift | 5 ++--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/MapboxVision/VisionManager/VisionManager.swift b/MapboxVision/VisionManager/VisionManager.swift index 010e03ae..4921a4d5 100644 --- a/MapboxVision/VisionManager/VisionManager.swift +++ b/MapboxVision/VisionManager/VisionManager.swift @@ -107,10 +107,7 @@ public final class VisionManager: BaseVisionManager { guard case .started = state else { throw VisionManagerError.startRecordingBeforeStart } -// dependencies.native.startRecordint(to: path) - -// dependencies.recorder.stop() -// dependencies.recorder.start(mode: .external(path: path)) + dependencies.native.startRecording(to: path) } /** @@ -125,10 +122,7 @@ public final class VisionManager: BaseVisionManager { assertionFailure("VisionManager should be started and recording") return } - // dependencies.native.stopRecording() - -// dependencies.recorder.stop() -// dependencies.recorder.start(mode: .internal) + dependencies.native.stopRecording() } /** diff --git a/MapboxVision/VisionManager/VisionManagerNativeProtocol.swift b/MapboxVision/VisionManager/VisionManagerNativeProtocol.swift index 4978332b..bfeff0b2 100644 --- a/MapboxVision/VisionManager/VisionManagerNativeProtocol.swift +++ b/MapboxVision/VisionManager/VisionManagerNativeProtocol.swift @@ -24,9 +24,8 @@ protocol VisionManagerNativeProtocol: VisionManagerBaseNativeProtocol { func destroy() - func getSeconds() -> Float - func startSavingSession(_ path: String) - func stopSavingSession() + func startRecording(to path: String) + func stopRecording() } extension VisionManagerBaseNative: VisionManagerBaseNativeProtocol {} From 39d313369ea090e2af9308ca8b1b722715748055 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 13 Nov 2019 10:24:55 +0100 Subject: [PATCH 13/39] Update recording flow --- MapboxVision/Core/Platform.swift | 25 +++++++- .../Recording/RecordCoordinator.swift | 2 +- .../Services/Recording/VideoRecorder.swift | 13 +++- .../Services/Recording/VideoTrimmer.swift | 60 ++++++++++++++++++- .../VisionManager/ManagerDependencies.swift | 2 + 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift index 54bc07df..44465635 100644 --- a/MapboxVision/Core/Platform.swift +++ b/MapboxVision/Core/Platform.swift @@ -6,6 +6,7 @@ typealias TelemetryFileMetadata = [String: String] final class Platform: NSObject { struct Dependencies { let recorder: VideoRecorder? + let videoTrimmer: VideoTrimmer? let eventsManager: EventsManager let archiver: Archiver? } @@ -43,7 +44,29 @@ extension Platform: PlatformInterface { dependencies.recorder?.stopRecording(completion: nil) } - func makeVideoClips(inputFilePath: String, outputDirectoryPath: String, clips: [VideoClip], callback: @escaping SuccessCallback) {} + func makeVideoClips(inputFilePath: String, clips: [VideoClip], callback: @escaping SuccessCallback) { + guard let videoTrimmer = dependencies.videoTrimmer else { + callback(false) + return + } + + var success = true + let group = DispatchGroup() + + for clip in clips { + group.enter() + videoTrimmer.trimVideo(source: inputFilePath, clip: clip) { error in + if error != nil { + success = false + } + group.leave() + } + } + + group.notify(queue: DispatchQueue.global(qos: .utility)) { + callback(success) + } + } func archiveFiles(filePaths: [String], archivePath: String, callback: @escaping SuccessCallback) { DispatchQueue.global(qos: .utility).async { diff --git a/MapboxVision/Services/Recording/RecordCoordinator.swift b/MapboxVision/Services/Recording/RecordCoordinator.swift index 1aeae5cf..760d41e7 100644 --- a/MapboxVision/Services/Recording/RecordCoordinator.swift +++ b/MapboxVision/Services/Recording/RecordCoordinator.swift @@ -254,7 +254,7 @@ final class RecordCoordinator { to: TimeInterval(request.clipEnd), settings: settings) { [jsonWriter, weak self] error in guard let self = self else { return } - if let trimError = (error as? VideoTrimmerError), case VideoTrimmerError.sourceIsNotExportable = trimError { + if let trimError = (error as? VideoTrimmerError), case VideoTrimmerError.sourceNotExportable = trimError { if self.trimRequestCache[chunk] != nil { self.trimRequestCache[chunk]?.append(request) } else { diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index d7f1635a..1d9249e6 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -38,7 +38,7 @@ final class VideoRecorder { currentAssetWriterInput = assetWriterInput assetWriterInput.expectsMediaDataInRealTime = true - writerQueue.async { + writerQueue.sync { let outputURL = URL(fileURLWithPath: path) let assetWriter: AVAssetWriter do { @@ -56,7 +56,7 @@ final class VideoRecorder { } func stopRecording(completion: (() -> Void)?) { - writerQueue.async { [weak self] in + writerQueue.sync { [weak self] in let cleanup = { self?.isRecording = false self?.currentAssetWriter = nil @@ -77,7 +77,14 @@ final class VideoRecorder { if let currentTime = self.currentTime { writer.endSession(atSourceTime: currentTime) } - writer.finishWriting(completionHandler: cleanup) + + let sem = DispatchSemaphore(value: 0) + writer.finishWriting { + sem.signal() + } + sem.wait() + + cleanup() } } diff --git a/MapboxVision/Services/Recording/VideoTrimmer.swift b/MapboxVision/Services/Recording/VideoTrimmer.swift index f68a9043..f4b4d2e0 100644 --- a/MapboxVision/Services/Recording/VideoTrimmer.swift +++ b/MapboxVision/Services/Recording/VideoTrimmer.swift @@ -2,9 +2,11 @@ import AVFoundation import Foundation private let timeScale: CMTimeScale = 600 +private let fileType: AVFileType = .mp4 enum VideoTrimmerError: LocalizedError { - case sourceIsNotExportable + case sourceNotExist + case sourceNotExportable } final class VideoTrimmer { @@ -22,7 +24,7 @@ final class VideoTrimmer { let asset = AVURLAsset(url: sourceURL, options: options) guard asset.isExportable else { - completion?(VideoTrimmerError.sourceIsNotExportable) + completion?(VideoTrimmerError.sourceNotExportable) return } @@ -60,4 +62,58 @@ final class VideoTrimmer { completion?(exportSession.error) } } + + func trimVideo(source: String, clip: VideoClip, completion: @escaping TrimCompletion) { + guard let sourceURL = URL(string: source) else { + completion(VideoTrimmerError.sourceNotExist) + return + } + + let options = [ + AVURLAssetPreferPreciseDurationAndTimingKey: true, + ] + + let asset = AVURLAsset(url: sourceURL, options: options) + guard asset.isExportable else { + completion(VideoTrimmerError.sourceNotExportable) + return + } + + let composition = AVMutableComposition() + guard + let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID()) + else { + assertionFailure("Unable to add video track to composition \(composition).") + return + } + + guard let videoAssetTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first else { + assertionFailure("Unable to obtain video track from asset \(asset).") + return + } + + let startTime = CMTime(seconds: Double(clip.startTime), preferredTimescale: timeScale) + let endTime = CMTime(seconds: Double(clip.stopTime), preferredTimescale: timeScale) + + let durationOfCurrentSlice = CMTimeSubtract(endTime, startTime) + let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTime, duration: durationOfCurrentSlice) + + do { + try videoTrack.insertTimeRange(timeRangeForCurrentSlice, of: videoAssetTrack, at: CMTime()) + } catch { + completion(error) + } + + guard + let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough) + else { return } + + exportSession.outputURL = URL(string: clip.path) + exportSession.outputFileType = fileType + exportSession.shouldOptimizeForNetworkUse = true + + exportSession.exportAsynchronously { + completion(exportSession.error) + } + } } diff --git a/MapboxVision/VisionManager/ManagerDependencies.swift b/MapboxVision/VisionManager/ManagerDependencies.swift index a4aa19bd..aa4a529c 100644 --- a/MapboxVision/VisionManager/ManagerDependencies.swift +++ b/MapboxVision/VisionManager/ManagerDependencies.swift @@ -38,6 +38,7 @@ struct VisionDependencies { let platform = Platform(dependencies: Platform.Dependencies( recorder: recorder, + videoTrimmer: VideoTrimmer(), eventsManager: eventsManager, archiver: recordArchiver )) @@ -81,6 +82,7 @@ struct ReplayDependencies { let platform = Platform(dependencies: Platform.Dependencies( recorder: nil, + videoTrimmer: nil, eventsManager: eventsManager, archiver: RecordArchiver() )) From 463065d8386e47fbce78b93d2d417c406da04b2d Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Fri, 15 Nov 2019 16:33:53 +0300 Subject: [PATCH 14/39] Use file URLs in VideoTrimmer --- MapboxVision/Services/Recording/VideoTrimmer.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/MapboxVision/Services/Recording/VideoTrimmer.swift b/MapboxVision/Services/Recording/VideoTrimmer.swift index f4b4d2e0..dd59c63f 100644 --- a/MapboxVision/Services/Recording/VideoTrimmer.swift +++ b/MapboxVision/Services/Recording/VideoTrimmer.swift @@ -5,7 +5,6 @@ private let timeScale: CMTimeScale = 600 private let fileType: AVFileType = .mp4 enum VideoTrimmerError: LocalizedError { - case sourceNotExist case sourceNotExportable } @@ -64,11 +63,7 @@ final class VideoTrimmer { } func trimVideo(source: String, clip: VideoClip, completion: @escaping TrimCompletion) { - guard let sourceURL = URL(string: source) else { - completion(VideoTrimmerError.sourceNotExist) - return - } - + let sourceURL = URL(fileURLWithPath: source) let options = [ AVURLAssetPreferPreciseDurationAndTimingKey: true, ] @@ -108,7 +103,7 @@ final class VideoTrimmer { let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough) else { return } - exportSession.outputURL = URL(string: clip.path) + exportSession.outputURL = URL(fileURLWithPath: clip.path) exportSession.outputFileType = fileType exportSession.shouldOptimizeForNetworkUse = true From 65784a007001fd7a4f006d827ba27826fd65d2b5 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Sat, 16 Nov 2019 07:43:37 +0300 Subject: [PATCH 15/39] Cleanup the unused code --- MapboxVision.xcodeproj/project.pbxproj | 152 +------- MapboxVision/Core/Platform.swift | 2 +- MapboxVision/Helpers/Country+SyncRegion.swift | 14 - MapboxVision/Helpers/MemoryByte.swift | 9 - MapboxVision/Helpers/TimeInterval.swift | 7 - MapboxVision/Helpers/URL.swift | 28 -- .../Helpers/UserDefaults+DefaultValue.swift | 9 - MapboxVision/Services/DataProvider.swift | 47 --- .../Services/DeviceInfoProvider.swift | 24 -- MapboxVision/Services/EventsManager.swift | 58 --- .../Services/Network/NetworkClient.swift | 1 - .../Services/Recording/FileManager.swift | 39 -- .../Services/Recording/FileRecorder.swift | 44 --- .../Services/Recording/ImageRecorder.swift | 16 - .../Recording/RecordCoordinator.swift | 361 ------------------ .../Services/Recording/RecordingQuota.swift | 70 ---- .../Services/Recording/SessionRecorder.swift | 129 ------- .../Services/Recording/VideoBuffer.swift | 102 ----- .../Services/Recording/VideoRecorder.swift | 8 +- .../Services/Recording/VideoTrimmer.swift | 51 --- MapboxVision/Services/SessionManager.swift | 53 --- .../Services/Storage/RecordDataSource.swift | 24 -- .../Services/Storage/RecordFileType.swift | 24 -- .../Services/Sync/ManagedSynchronizer.swift | 77 ---- .../Services/Sync/RecordSynchronizer.swift | 270 ------------- .../Services/Sync/Synchronizable.swift | 15 - .../VisionManager/ManagerDependencies.swift | 38 +- .../VisionManager/OperationMode.swift | 65 ---- .../SessionRecorderProtocol.swift | 19 - .../VisionManager/VisionManager.swift | 84 ---- MapboxVisionTests/DeviceCheckerTests.swift | 12 + .../DeviceInfoProviderTests.swift | 41 -- .../Helpers/ClosureSyncDelegate.swift | 16 - MapboxVisionTests/Mocks/MockArchiver.swift | 10 - MapboxVisionTests/Mocks/MockFileManager.swift | 43 --- .../Mocks/MockFrameRecorder.swift | 24 ++ MapboxVisionTests/Mocks/MockNative.swift | 4 + .../Mocks/MockNetworkClient.swift | 19 - MapboxVisionTests/Mocks/MockPlatform.swift | 9 - .../Mocks/MockRecordDataSource.swift | 10 - .../Mocks/MockSessionRecorder.swift | 52 --- .../Mocks/MockSynchronizable.swift | 47 --- MapboxVisionTests/Mocks/UIDeviceStubs.swift | 20 +- .../RecordCoordinatorTests.swift | 248 ------------ .../RecordSynchronizerTests.swift | 152 -------- MapboxVisionTests/RecordingQuotaTests.swift | 109 ------ MapboxVisionTests/VisionManagerTests.swift | 285 +------------- 47 files changed, 71 insertions(+), 2870 deletions(-) delete mode 100644 MapboxVision/Helpers/Country+SyncRegion.swift delete mode 100644 MapboxVision/Helpers/MemoryByte.swift delete mode 100644 MapboxVision/Helpers/TimeInterval.swift delete mode 100644 MapboxVision/Helpers/URL.swift delete mode 100644 MapboxVision/Helpers/UserDefaults+DefaultValue.swift delete mode 100644 MapboxVision/Services/DeviceInfoProvider.swift delete mode 100644 MapboxVision/Services/Recording/FileManager.swift delete mode 100644 MapboxVision/Services/Recording/FileRecorder.swift delete mode 100644 MapboxVision/Services/Recording/ImageRecorder.swift delete mode 100644 MapboxVision/Services/Recording/RecordCoordinator.swift delete mode 100644 MapboxVision/Services/Recording/RecordingQuota.swift delete mode 100644 MapboxVision/Services/Recording/SessionRecorder.swift delete mode 100644 MapboxVision/Services/Recording/VideoBuffer.swift delete mode 100644 MapboxVision/Services/SessionManager.swift delete mode 100644 MapboxVision/Services/Storage/RecordDataSource.swift delete mode 100644 MapboxVision/Services/Storage/RecordFileType.swift delete mode 100644 MapboxVision/Services/Sync/ManagedSynchronizer.swift delete mode 100644 MapboxVision/Services/Sync/RecordSynchronizer.swift delete mode 100644 MapboxVision/Services/Sync/Synchronizable.swift delete mode 100644 MapboxVision/VisionManager/OperationMode.swift delete mode 100644 MapboxVision/VisionManager/SessionRecorderProtocol.swift delete mode 100644 MapboxVisionTests/DeviceInfoProviderTests.swift delete mode 100644 MapboxVisionTests/Helpers/ClosureSyncDelegate.swift delete mode 100644 MapboxVisionTests/Mocks/MockArchiver.swift delete mode 100644 MapboxVisionTests/Mocks/MockFileManager.swift create mode 100644 MapboxVisionTests/Mocks/MockFrameRecorder.swift delete mode 100644 MapboxVisionTests/Mocks/MockNetworkClient.swift delete mode 100644 MapboxVisionTests/Mocks/MockPlatform.swift delete mode 100644 MapboxVisionTests/Mocks/MockRecordDataSource.swift delete mode 100644 MapboxVisionTests/Mocks/MockSessionRecorder.swift delete mode 100644 MapboxVisionTests/Mocks/MockSynchronizable.swift delete mode 100644 MapboxVisionTests/RecordCoordinatorTests.swift delete mode 100644 MapboxVisionTests/RecordSynchronizerTests.swift delete mode 100644 MapboxVisionTests/RecordingQuotaTests.swift diff --git a/MapboxVision.xcodeproj/project.pbxproj b/MapboxVision.xcodeproj/project.pbxproj index 20517e86..1bcda533 100644 --- a/MapboxVision.xcodeproj/project.pbxproj +++ b/MapboxVision.xcodeproj/project.pbxproj @@ -22,8 +22,7 @@ 2E071AA22257CCEA00345FA8 /* MapboxVisionSafetyNative.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E071AA12257CCEA00345FA8 /* MapboxVisionSafetyNative.framework */; }; 2E19772B211DDD5900311B95 /* DeviceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E19772A211DDD5900311B95 /* DeviceChecker.swift */; }; 2E1FC07B22D4BE1A009BEBFB /* RecordingPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E1FC07A22D4BE1A009BEBFB /* RecordingPathTests.swift */; }; - 2E22F1992108870B0093F043 /* VideoBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E22F1982108870B0093F043 /* VideoBuffer.swift */; }; - 2E245B1C215A57D2008F57D1 /* OperationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E245B1B215A57D2008F57D1 /* OperationMode.swift */; }; + 2E283BD7237FABB700920228 /* MockFrameRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E283BD6237FABB700920228 /* MockFrameRecorder.swift */; }; 2E2B828623058986001494E5 /* FileManager+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E2B828523058986001494E5 /* FileManager+Helpers.swift */; }; 2E4EE21720EFD378003431B3 /* MapboxVision.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E4EE21520EFD378003431B3 /* MapboxVision.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2E4EE24D20F05045003431B3 /* VisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23E20EFD651003431B3 /* VisionManager.swift */; }; @@ -33,18 +32,10 @@ 2E4EE27420F05652003431B3 /* CoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23920EFD5BB003431B3 /* CoreConfig.swift */; }; 2E4EE27720F05659003431B3 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22020EFD5BA003431B3 /* NetworkClient.swift */; }; 2E4EE27820F05659003431B3 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22120EFD5BA003431B3 /* Path.swift */; }; - 2E4EE27A20F0565E003431B3 /* RecordDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22420EFD5BA003431B3 /* RecordDataSource.swift */; }; - 2E4EE27B20F0565E003431B3 /* RecordFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22520EFD5BA003431B3 /* RecordFileType.swift */; }; - 2E4EE27C20F05661003431B3 /* FileRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22720EFD5BA003431B3 /* FileRecorder.swift */; }; 2E4EE27D20F05661003431B3 /* VideoRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22820EFD5BA003431B3 /* VideoRecorder.swift */; }; - 2E4EE27E20F05661003431B3 /* ImageRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22920EFD5BA003431B3 /* ImageRecorder.swift */; }; - 2E4EE27F20F05661003431B3 /* RecordCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22A20EFD5BA003431B3 /* RecordCoordinator.swift */; }; 2E4EE28020F05661003431B3 /* RecordingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22B20EFD5BA003431B3 /* RecordingPath.swift */; }; - 2E4EE28320F05665003431B3 /* DeviceInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22F20EFD5BA003431B3 /* DeviceInfoProvider.swift */; }; 2E4EE28420F05665003431B3 /* ManagerDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23020EFD5BA003431B3 /* ManagerDependencies.swift */; }; 2E4EE28820F05665003431B3 /* DataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23420EFD5BA003431B3 /* DataProvider.swift */; }; - 2E4EE29420F05844003431B3 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE29220F05843003431B3 /* URL.swift */; }; - 2E4EE2A020F058B9003431B3 /* UserDefaults+DefaultValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE29920F058B6003431B3 /* UserDefaults+DefaultValue.swift */; }; 2E4EE2A120F058B9003431B3 /* Collection+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE29A20F058B7003431B3 /* Collection+Safe.swift */; }; 2E4EE2A220F058B9003431B3 /* AVCaptureConnection+Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE29B20F058B7003431B3 /* AVCaptureConnection+Orientation.swift */; }; 2E4EE2AA20F0590F003431B3 /* MotionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE2A520F0590B003431B3 /* MotionManager.swift */; }; @@ -53,38 +44,23 @@ 2E4EE2CC20F06A11003431B3 /* CGPoint+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE2CB20F06A11003431B3 /* CGPoint+Convert.swift */; }; 2E4EE2E520F0CC63003431B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2E4EE2E220F0CC63003431B3 /* Assets.xcassets */; }; 2E50E4AE22FC7B2C00A2C03D /* SyncRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E50E4AD22FC7B2C00A2C03D /* SyncRegion.swift */; }; - 2E539DE9210F00190058D9B2 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E539DE8210F00190058D9B2 /* SessionManager.swift */; }; 2E6033542142900C0050EDB3 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E6033532142900C0050EDB3 /* Images.swift */; }; - 2E7F14EA22896C5700BAF585 /* Synchronizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14E922896C5700BAF585 /* Synchronizable.swift */; }; - 2E7F14EC22896D0600BAF585 /* ManagedSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14EB22896D0600BAF585 /* ManagedSynchronizer.swift */; }; - 2E7F14EE22898EF200BAF585 /* SessionRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14ED22898EF200BAF585 /* SessionRecorder.swift */; }; 2E7F14FB228E2D1400BAF585 /* BaseVisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FA228E2D1400BAF585 /* BaseVisionManager.swift */; }; 2E7F14FD228E92D900BAF585 /* VisionReplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FC228E92D900BAF585 /* VisionReplayManager.swift */; }; 2E7F14FF228E982D00BAF585 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FE228E982D00BAF585 /* VideoPlayer.swift */; }; 2E7F1501228F0E0B00BAF585 /* VisionManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F1500228F0E0B00BAF585 /* VisionManagerProtocol.swift */; }; 2E81BBF1222E9FA900C4B49C /* VisionManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E81BBF0222E9FA900C4B49C /* VisionManagerDelegate.swift */; }; 2E88A9BA213ED5DC008A27C8 /* ModelPerformance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88A9B9213ED5DC008A27C8 /* ModelPerformance.swift */; }; - 2EBE10E72301674400CE87CE /* ClosureSyncDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE10E62301674400CE87CE /* ClosureSyncDelegate.swift */; }; - 2EBE10E92301816600CE87CE /* Country+SyncRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE10E82301816600CE87CE /* Country+SyncRegion.swift */; }; 2EC332A3217E02BA00E041FE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC332A2217E02BA00E041FE /* String.swift */; }; 2EDD8707219ED89E00760C6D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDD8706219ED89E00760C6D /* Constants.swift */; }; - 2EDD8709219EDC0200760C6D /* DeviceInfoProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDD8708219EDC0200760C6D /* DeviceInfoProviderTests.swift */; }; 2EDFFD73224BBD3E003BB718 /* ObservableVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDFFD72224BBD3E003BB718 /* ObservableVideoSource.swift */; }; - 2EF31C9021904BB300752D23 /* RecordCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF31C8F21904BB300752D23 /* RecordCoordinatorTests.swift */; }; 2EFF563F210B4A5B00B8D512 /* VideoTrimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EFF563E210B4A5B00B8D512 /* VideoTrimmer.swift */; }; - 3A0D52F722EC5F0A00E60FCF /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0D52F622EC5F0A00E60FCF /* TimeInterval.swift */; }; - 3A0D52F922EC74EA00E60FCF /* MemoryByte.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0D52F822EC74EA00E60FCF /* MemoryByte.swift */; }; 3A11FEDF2282BCD300B68E5B /* DeviceCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */; }; 3A7676AE22D647FE00ABC483 /* DeviceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E19772A211DDD5900311B95 /* DeviceChecker.swift */; }; 3AAB2EFB22858F28000D052E /* UIDeviceStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB2EFA22858F28000D052E /* UIDeviceStubs.swift */; }; - 3ACF140022A955FD00EA3255 /* RecordingQuotaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF13FF22A955FD00EA3255 /* RecordingQuotaTests.swift */; }; 5A006F4A231546250097FADF /* VisionARViewController+ARManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A006F49231546250097FADF /* VisionARViewController+ARManager.swift */; }; 5A006F58231570FC0097FADF /* VideoSourceObserverProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A006F57231570FC0097FADF /* VideoSourceObserverProxy.swift */; }; 5A3C9D8722C37AF700700DC6 /* MockVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D8622C37AF700700DC6 /* MockVideoSource.swift */; }; - 5A3C9D8922C37CCC00700DC6 /* MockSynchronizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D8822C37CCC00700DC6 /* MockSynchronizable.swift */; }; - 5A3C9D8B22C37DE200700DC6 /* MockPlatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D8A22C37DE200700DC6 /* MockPlatform.swift */; }; - 5A3C9D8D22C381EB00700DC6 /* MockSessionRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D8C22C381EB00700DC6 /* MockSessionRecorder.swift */; }; - 5A3C9D8F22C3826F00700DC6 /* SessionRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D8E22C3826F00700DC6 /* SessionRecorderProtocol.swift */; }; 5A3C9D9122C39F5300700DC6 /* MockDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D9022C39F5300700DC6 /* MockDataProvider.swift */; }; 5A3C9D9322C3B1C300700DC6 /* VisionManagerNativeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D9222C3B1C300700DC6 /* VisionManagerNativeProtocol.swift */; }; 5A3C9D9922C50D0000700DC6 /* MockNative.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3C9D9822C50D0000700DC6 /* MockNative.swift */; }; @@ -92,22 +68,14 @@ 5A3E42C922B28EAA0035A010 /* VisionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3E42C422B280180035A010 /* VisionManagerTests.swift */; }; 9D72A196BCA3C43852E39A10 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72A773F7BCE045156649B6 /* DateFormatter.swift */; }; 9D72A4A4A30F19F3EA8A7316 /* VideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72A9D70E54F426D0E8990E /* VideoSource.swift */; }; - 9D72A5C114A06457134C5EE5 /* RecordSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72AEC4FF135C1A496B5215 /* RecordSynchronizer.swift */; }; 9D72AB0D4BB86199CF051DC2 /* CVPixelBuffer+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72A5D7CECFACCEE667A42B /* CVPixelBuffer+Helper.swift */; }; 9D72AB1C8F3CE6B6E2222E7E /* CameraVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72A648B7F1E55B41E25240 /* CameraVideoSource.swift */; }; 9D72ACF3C976A03345D44F97 /* DocumentsLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72A2983A81E002804CA22C /* DocumentsLocation.swift */; }; - B50CFBAA2163C89D00C9C5A5 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CFBA92163C89D00C9C5A5 /* FileManager.swift */; }; - B50CFBAD2163C91500C9C5A5 /* MockNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CFBAC2163C91500C9C5A5 /* MockNetworkClient.swift */; }; - B50CFBAF2163C98E00C9C5A5 /* MockRecordDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CFBAE2163C98E00C9C5A5 /* MockRecordDataSource.swift */; }; - B50CFBB12163C9AC00C9C5A5 /* MockArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CFBB02163C9AC00C9C5A5 /* MockArchiver.swift */; }; - B50CFBB32163C9C500C9C5A5 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CFBB22163C9C500C9C5A5 /* MockFileManager.swift */; }; - B51E557C215E313500650DDE /* RecordSynchronizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51E557B215E313500650DDE /* RecordSynchronizerTests.swift */; }; B51E557E215E313500650DDE /* MapboxVision.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E4EE21220EFD378003431B3 /* MapboxVision.framework */; }; B53B75B921144AE9001D65AC /* MapboxMobileEvents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B53B75B821144AE9001D65AC /* MapboxMobileEvents.framework */; }; B53B75BB21144B9A001D65AC /* EventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53B75BA21144B9A001D65AC /* EventsManager.swift */; }; B583DF832240F55D00D73673 /* ZIPFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B583DF822240F55D00D73673 /* ZIPFoundation.framework */; }; B5AEB46A21186CCF00AF198E /* RecordArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEB46921186CCF00AF198E /* RecordArchiver.swift */; }; - B5BD9F6F211C69FF001EE97F /* RecordingQuota.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD9F6E211C69FF001EE97F /* RecordingQuota.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -136,22 +104,15 @@ 2E071AA12257CCEA00345FA8 /* MapboxVisionSafetyNative.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MapboxVisionSafetyNative.framework; path = Carthage/Build/iOS/MapboxVisionSafetyNative.framework; sourceTree = ""; }; 2E19772A211DDD5900311B95 /* DeviceChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceChecker.swift; sourceTree = ""; }; 2E1FC07A22D4BE1A009BEBFB /* RecordingPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingPathTests.swift; sourceTree = ""; }; - 2E22F1982108870B0093F043 /* VideoBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBuffer.swift; sourceTree = ""; }; - 2E245B1B215A57D2008F57D1 /* OperationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMode.swift; sourceTree = ""; }; + 2E283BD6237FABB700920228 /* MockFrameRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFrameRecorder.swift; sourceTree = ""; }; 2E2B828523058986001494E5 /* FileManager+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Helpers.swift"; sourceTree = ""; }; 2E4EE21220EFD378003431B3 /* MapboxVision.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MapboxVision.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2E4EE21520EFD378003431B3 /* MapboxVision.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapboxVision.h; sourceTree = ""; }; 2E4EE21620EFD378003431B3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2E4EE22020EFD5BA003431B3 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 2E4EE22120EFD5BA003431B3 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = ""; }; - 2E4EE22420EFD5BA003431B3 /* RecordDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordDataSource.swift; sourceTree = ""; }; - 2E4EE22520EFD5BA003431B3 /* RecordFileType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordFileType.swift; sourceTree = ""; }; - 2E4EE22720EFD5BA003431B3 /* FileRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileRecorder.swift; sourceTree = ""; }; 2E4EE22820EFD5BA003431B3 /* VideoRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRecorder.swift; sourceTree = ""; }; - 2E4EE22920EFD5BA003431B3 /* ImageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRecorder.swift; sourceTree = ""; }; - 2E4EE22A20EFD5BA003431B3 /* RecordCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordCoordinator.swift; sourceTree = ""; }; 2E4EE22B20EFD5BA003431B3 /* RecordingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingPath.swift; sourceTree = ""; }; - 2E4EE22F20EFD5BA003431B3 /* DeviceInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoProvider.swift; sourceTree = ""; }; 2E4EE23020EFD5BA003431B3 /* ManagerDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagerDependencies.swift; sourceTree = ""; }; 2E4EE23420EFD5BA003431B3 /* DataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvider.swift; sourceTree = ""; }; 2E4EE23820EFD5BB003431B3 /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; @@ -159,8 +120,6 @@ 2E4EE23E20EFD651003431B3 /* VisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionManager.swift; sourceTree = ""; }; 2E4EE23F20EFD651003431B3 /* VisionPresentationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionPresentationViewController.swift; sourceTree = ""; }; 2E4EE25420F05370003431B3 /* VideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoSettings.swift; sourceTree = ""; }; - 2E4EE29220F05843003431B3 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; - 2E4EE29920F058B6003431B3 /* UserDefaults+DefaultValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+DefaultValue.swift"; sourceTree = ""; }; 2E4EE29A20F058B7003431B3 /* Collection+Safe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Safe.swift"; sourceTree = ""; }; 2E4EE29B20F058B7003431B3 /* AVCaptureConnection+Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureConnection+Orientation.swift"; sourceTree = ""; }; 2E4EE2A520F0590B003431B3 /* MotionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionManager.swift; sourceTree = ""; }; @@ -169,38 +128,23 @@ 2E4EE2CB20F06A11003431B3 /* CGPoint+Convert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Convert.swift"; sourceTree = ""; }; 2E4EE2E220F0CC63003431B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2E50E4AD22FC7B2C00A2C03D /* SyncRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncRegion.swift; sourceTree = ""; }; - 2E539DE8210F00190058D9B2 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 2E6033532142900C0050EDB3 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; - 2E7F14E922896C5700BAF585 /* Synchronizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Synchronizable.swift; sourceTree = ""; }; - 2E7F14EB22896D0600BAF585 /* ManagedSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizer.swift; sourceTree = ""; }; - 2E7F14ED22898EF200BAF585 /* SessionRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRecorder.swift; sourceTree = ""; }; 2E7F14FA228E2D1400BAF585 /* BaseVisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVisionManager.swift; sourceTree = ""; }; 2E7F14FC228E92D900BAF585 /* VisionReplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionReplayManager.swift; sourceTree = ""; }; 2E7F14FE228E982D00BAF585 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 2E7F1500228F0E0B00BAF585 /* VisionManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionManagerProtocol.swift; sourceTree = ""; }; 2E81BBF0222E9FA900C4B49C /* VisionManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionManagerDelegate.swift; sourceTree = ""; }; 2E88A9B9213ED5DC008A27C8 /* ModelPerformance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelPerformance.swift; sourceTree = ""; }; - 2EBE10E62301674400CE87CE /* ClosureSyncDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureSyncDelegate.swift; sourceTree = ""; }; - 2EBE10E82301816600CE87CE /* Country+SyncRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Country+SyncRegion.swift"; sourceTree = ""; }; 2EC332A2217E02BA00E041FE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2EDD8706219ED89E00760C6D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 2EDD8708219EDC0200760C6D /* DeviceInfoProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoProviderTests.swift; sourceTree = ""; }; 2EDFFD72224BBD3E003BB718 /* ObservableVideoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableVideoSource.swift; sourceTree = ""; }; - 2EF31C8F21904BB300752D23 /* RecordCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordCoordinatorTests.swift; sourceTree = ""; }; 2EFF563E210B4A5B00B8D512 /* VideoTrimmer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoTrimmer.swift; sourceTree = ""; }; - 3A0D52F622EC5F0A00E60FCF /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; - 3A0D52F822EC74EA00E60FCF /* MemoryByte.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryByte.swift; sourceTree = ""; }; 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheckerTests.swift; sourceTree = ""; }; 3A7E992C22A0299A005EB875 /* MapboxVisionARNative.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MapboxVisionARNative.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AAB2EFA22858F28000D052E /* UIDeviceStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceStubs.swift; sourceTree = ""; }; - 3ACF13FF22A955FD00EA3255 /* RecordingQuotaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingQuotaTests.swift; sourceTree = ""; }; 5A006F49231546250097FADF /* VisionARViewController+ARManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VisionARViewController+ARManager.swift"; sourceTree = ""; }; 5A006F57231570FC0097FADF /* VideoSourceObserverProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoSourceObserverProxy.swift; sourceTree = ""; }; 5A3C9D8622C37AF700700DC6 /* MockVideoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVideoSource.swift; sourceTree = ""; }; - 5A3C9D8822C37CCC00700DC6 /* MockSynchronizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSynchronizable.swift; sourceTree = ""; }; - 5A3C9D8A22C37DE200700DC6 /* MockPlatform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPlatform.swift; sourceTree = ""; }; - 5A3C9D8C22C381EB00700DC6 /* MockSessionRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionRecorder.swift; sourceTree = ""; }; - 5A3C9D8E22C3826F00700DC6 /* SessionRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRecorderProtocol.swift; sourceTree = ""; }; 5A3C9D9022C39F5300700DC6 /* MockDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataProvider.swift; sourceTree = ""; }; 5A3C9D9222C3B1C300700DC6 /* VisionManagerNativeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionManagerNativeProtocol.swift; sourceTree = ""; }; 5A3C9D9822C50D0000700DC6 /* MockNative.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNative.swift; sourceTree = ""; }; @@ -211,20 +155,12 @@ 9D72A648B7F1E55B41E25240 /* CameraVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraVideoSource.swift; sourceTree = ""; }; 9D72A773F7BCE045156649B6 /* DateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 9D72A9D70E54F426D0E8990E /* VideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoSource.swift; sourceTree = ""; }; - 9D72AEC4FF135C1A496B5215 /* RecordSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordSynchronizer.swift; sourceTree = ""; }; - B50CFBA92163C89D00C9C5A5 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; - B50CFBAC2163C91500C9C5A5 /* MockNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkClient.swift; sourceTree = ""; }; - B50CFBAE2163C98E00C9C5A5 /* MockRecordDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRecordDataSource.swift; sourceTree = ""; }; - B50CFBB02163C9AC00C9C5A5 /* MockArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockArchiver.swift; sourceTree = ""; }; - B50CFBB22163C9C500C9C5A5 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; B51E5579215E313500650DDE /* MapboxVisionTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapboxVisionTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B51E557B215E313500650DDE /* RecordSynchronizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordSynchronizerTests.swift; sourceTree = ""; }; B51E557D215E313500650DDE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B53B75B821144AE9001D65AC /* MapboxMobileEvents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapboxMobileEvents.framework; path = Carthage/Build/iOS/MapboxMobileEvents.framework; sourceTree = SOURCE_ROOT; }; B53B75BA21144B9A001D65AC /* EventsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsManager.swift; sourceTree = ""; }; B583DF822240F55D00D73673 /* ZIPFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZIPFoundation.framework; path = Carthage/Build/iOS/ZIPFoundation.framework; sourceTree = ""; }; B5AEB46921186CCF00AF198E /* RecordArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordArchiver.swift; sourceTree = ""; }; - B5BD9F6E211C69FF001EE97F /* RecordingQuota.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingQuota.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -295,12 +231,10 @@ children = ( 2E7F14FA228E2D1400BAF585 /* BaseVisionManager.swift */, 2E4EE23020EFD5BA003431B3 /* ManagerDependencies.swift */, - 2E245B1B215A57D2008F57D1 /* OperationMode.swift */, 2E7F14FC228E92D900BAF585 /* VisionReplayManager.swift */, 2E4EE23E20EFD651003431B3 /* VisionManager.swift */, 2E81BBF0222E9FA900C4B49C /* VisionManagerDelegate.swift */, 2E7F1500228F0E0B00BAF585 /* VisionManagerProtocol.swift */, - 5A3C9D8E22C3826F00700DC6 /* SessionRecorderProtocol.swift */, 5A3C9D9222C3B1C300700DC6 /* VisionManagerNativeProtocol.swift */, ); path = VisionManager; @@ -371,29 +305,13 @@ path = Network; sourceTree = ""; }; - 2E4EE22620EFD5BA003431B3 /* Storage */ = { - isa = PBXGroup; - children = ( - 2E4EE22420EFD5BA003431B3 /* RecordDataSource.swift */, - 2E4EE22520EFD5BA003431B3 /* RecordFileType.swift */, - ); - path = Storage; - sourceTree = ""; - }; 2E4EE22E20EFD5BA003431B3 /* Recording */ = { isa = PBXGroup; children = ( - 2E4EE22720EFD5BA003431B3 /* FileRecorder.swift */, 2E4EE22820EFD5BA003431B3 /* VideoRecorder.swift */, - 2E4EE22920EFD5BA003431B3 /* ImageRecorder.swift */, - 2E4EE22A20EFD5BA003431B3 /* RecordCoordinator.swift */, 2E4EE22B20EFD5BA003431B3 /* RecordingPath.swift */, - 2E22F1982108870B0093F043 /* VideoBuffer.swift */, 2EFF563E210B4A5B00B8D512 /* VideoTrimmer.swift */, B5AEB46921186CCF00AF198E /* RecordArchiver.swift */, - B5BD9F6E211C69FF001EE97F /* RecordingQuota.swift */, - B50CFBA92163C89D00C9C5A5 /* FileManager.swift */, - 2E7F14ED22898EF200BAF585 /* SessionRecorder.swift */, 9D72A2983A81E002804CA22C /* DocumentsLocation.swift */, ); path = Recording; @@ -404,17 +322,13 @@ children = ( 2E4EE22320EFD5BA003431B3 /* Network */, 2E4EE22E20EFD5BA003431B3 /* Recording */, - 2E4EE22620EFD5BA003431B3 /* Storage */, - 9D72AF1C0C0DA63A86960F57 /* Sync */, 9D72AC788D4F1F83614B3374 /* VideoSource */, 2EDD8706219ED89E00760C6D /* Constants.swift */, 2E4EE23420EFD5BA003431B3 /* DataProvider.swift */, - 2E4EE22F20EFD5BA003431B3 /* DeviceInfoProvider.swift */, B53B75BA21144B9A001D65AC /* EventsManager.swift */, 2E4EE2A720F0590D003431B3 /* LocationManager.swift */, 2E88A9B9213ED5DC008A27C8 /* ModelPerformance.swift */, 2E4EE2A520F0590B003431B3 /* MotionManager.swift */, - 2E539DE8210F00190058D9B2 /* SessionManager.swift */, 2E50E4AD22FC7B2C00A2C03D /* SyncRegion.swift */, 2E7F14FE228E982D00BAF585 /* VideoPlayer.swift */, 2E4EE25420F05370003431B3 /* VideoSettings.swift */, @@ -445,16 +359,11 @@ 2E4EE29B20F058B7003431B3 /* AVCaptureConnection+Orientation.swift */, 2E4EE2CB20F06A11003431B3 /* CGPoint+Convert.swift */, 2E4EE29A20F058B7003431B3 /* Collection+Safe.swift */, - 2EBE10E82301816600CE87CE /* Country+SyncRegion.swift */, 9D72A5D7CECFACCEE667A42B /* CVPixelBuffer+Helper.swift */, 9D72A773F7BCE045156649B6 /* DateFormatter.swift */, 2E19772A211DDD5900311B95 /* DeviceChecker.swift */, - 3A0D52F822EC74EA00E60FCF /* MemoryByte.swift */, 2E4EE2B820F05ACB003431B3 /* Result.swift */, 2EC332A2217E02BA00E041FE /* String.swift */, - 3A0D52F622EC5F0A00E60FCF /* TimeInterval.swift */, - 2E4EE29220F05843003431B3 /* URL.swift */, - 2E4EE29920F058B6003431B3 /* UserDefaults+DefaultValue.swift */, ); path = Helpers; sourceTree = ""; @@ -484,7 +393,6 @@ 2EBE10E52301672C00CE87CE /* Helpers */ = { isa = PBXGroup; children = ( - 2EBE10E62301674400CE87CE /* ClosureSyncDelegate.swift */, 2E2B828523058986001494E5 /* FileManager+Helpers.swift */, ); path = Helpers; @@ -509,31 +417,15 @@ path = VideoSource; sourceTree = ""; }; - 9D72AF1C0C0DA63A86960F57 /* Sync */ = { - isa = PBXGroup; - children = ( - 9D72AEC4FF135C1A496B5215 /* RecordSynchronizer.swift */, - 2E7F14E922896C5700BAF585 /* Synchronizable.swift */, - 2E7F14EB22896D0600BAF585 /* ManagedSynchronizer.swift */, - ); - path = Sync; - sourceTree = ""; - }; B50CFBAB2163C8D700C9C5A5 /* Mocks */ = { isa = PBXGroup; children = ( - B50CFBAC2163C91500C9C5A5 /* MockNetworkClient.swift */, - B50CFBAE2163C98E00C9C5A5 /* MockRecordDataSource.swift */, - B50CFBB02163C9AC00C9C5A5 /* MockArchiver.swift */, - B50CFBB22163C9C500C9C5A5 /* MockFileManager.swift */, 3AAB2EFA22858F28000D052E /* UIDeviceStubs.swift */, 5A3C9D8622C37AF700700DC6 /* MockVideoSource.swift */, - 5A3C9D8822C37CCC00700DC6 /* MockSynchronizable.swift */, - 5A3C9D8A22C37DE200700DC6 /* MockPlatform.swift */, - 5A3C9D8C22C381EB00700DC6 /* MockSessionRecorder.swift */, 5A3C9D9022C39F5300700DC6 /* MockDataProvider.swift */, 5A3C9D9822C50D0000700DC6 /* MockNative.swift */, 5A3C9D9A22C50D7C00700DC6 /* MockSensors.swift */, + 2E283BD6237FABB700920228 /* MockFrameRecorder.swift */, ); path = Mocks; sourceTree = ""; @@ -543,10 +435,6 @@ children = ( 2EBE10E52301672C00CE87CE /* Helpers */, B50CFBAB2163C8D700C9C5A5 /* Mocks */, - 3ACF13FF22A955FD00EA3255 /* RecordingQuotaTests.swift */, - B51E557B215E313500650DDE /* RecordSynchronizerTests.swift */, - 2EF31C8F21904BB300752D23 /* RecordCoordinatorTests.swift */, - 2EDD8708219EDC0200760C6D /* DeviceInfoProviderTests.swift */, 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */, B51E557D215E313500650DDE /* Info.plist */, 2E1FC07A22D4BE1A009BEBFB /* RecordingPathTests.swift */, @@ -831,64 +719,43 @@ buildActionMask = 2147483647; files = ( 2E7F14FF228E982D00BAF585 /* VideoPlayer.swift in Sources */, - 3A0D52F722EC5F0A00E60FCF /* TimeInterval.swift in Sources */, 2E50E4AE22FC7B2C00A2C03D /* SyncRegion.swift in Sources */, 2E4EE2BA20F05ACB003431B3 /* Result.swift in Sources */, 2E4EE27320F05652003431B3 /* Platform.swift in Sources */, 2E19772B211DDD5900311B95 /* DeviceChecker.swift in Sources */, 2E4EE27820F05659003431B3 /* Path.swift in Sources */, - 2E245B1C215A57D2008F57D1 /* OperationMode.swift in Sources */, - 2E4EE27A20F0565E003431B3 /* RecordDataSource.swift in Sources */, 2E4EE25520F05371003431B3 /* VideoSettings.swift in Sources */, B53B75BB21144B9A001D65AC /* EventsManager.swift in Sources */, 5A006F58231570FC0097FADF /* VideoSourceObserverProxy.swift in Sources */, 2E4EE28420F05665003431B3 /* ManagerDependencies.swift in Sources */, - 3A0D52F922EC74EA00E60FCF /* MemoryByte.swift in Sources */, - 5A3C9D8F22C3826F00700DC6 /* SessionRecorderProtocol.swift in Sources */, 2E4EE27720F05659003431B3 /* NetworkClient.swift in Sources */, 2E4EE2AC20F0590F003431B3 /* LocationManager.swift in Sources */, 2E4EE2A220F058B9003431B3 /* AVCaptureConnection+Orientation.swift in Sources */, 2E4EE24D20F05045003431B3 /* VisionManager.swift in Sources */, - 2E4EE2A020F058B9003431B3 /* UserDefaults+DefaultValue.swift in Sources */, 2E4EE28020F05661003431B3 /* RecordingPath.swift in Sources */, - 2E4EE29420F05844003431B3 /* URL.swift in Sources */, 2E4EE28820F05665003431B3 /* DataProvider.swift in Sources */, 2E4EE2CC20F06A11003431B3 /* CGPoint+Convert.swift in Sources */, 2EDD8707219ED89E00760C6D /* Constants.swift in Sources */, 2E88A9BA213ED5DC008A27C8 /* ModelPerformance.swift in Sources */, 2E4EE24E20F05045003431B3 /* VisionPresentationViewController.swift in Sources */, - 2E4EE28320F05665003431B3 /* DeviceInfoProvider.swift in Sources */, - B5BD9F6F211C69FF001EE97F /* RecordingQuota.swift in Sources */, 2EFF563F210B4A5B00B8D512 /* VideoTrimmer.swift in Sources */, 2E4EE2A120F058B9003431B3 /* Collection+Safe.swift in Sources */, 2E7F14FD228E92D900BAF585 /* VisionReplayManager.swift in Sources */, 2E6033542142900C0050EDB3 /* Images.swift in Sources */, - 2E4EE27B20F0565E003431B3 /* RecordFileType.swift in Sources */, 2E4EE27D20F05661003431B3 /* VideoRecorder.swift in Sources */, 2E81BBF1222E9FA900C4B49C /* VisionManagerDelegate.swift in Sources */, - 2E539DE9210F00190058D9B2 /* SessionManager.swift in Sources */, - 2E7F14EC22896D0600BAF585 /* ManagedSynchronizer.swift in Sources */, 2E4EE2AA20F0590F003431B3 /* MotionManager.swift in Sources */, 2E7F14FB228E2D1400BAF585 /* BaseVisionManager.swift in Sources */, - 2E7F14EA22896C5700BAF585 /* Synchronizable.swift in Sources */, - B50CFBAA2163C89D00C9C5A5 /* FileManager.swift in Sources */, - 2E4EE27C20F05661003431B3 /* FileRecorder.swift in Sources */, 2E7F1501228F0E0B00BAF585 /* VisionManagerProtocol.swift in Sources */, B5AEB46A21186CCF00AF198E /* RecordArchiver.swift in Sources */, - 2E4EE27F20F05661003431B3 /* RecordCoordinator.swift in Sources */, - 2E22F1992108870B0093F043 /* VideoBuffer.swift in Sources */, 2EC332A3217E02BA00E041FE /* String.swift in Sources */, 2EDFFD73224BBD3E003BB718 /* ObservableVideoSource.swift in Sources */, - 2E4EE27E20F05661003431B3 /* ImageRecorder.swift in Sources */, - 2E7F14EE22898EF200BAF585 /* SessionRecorder.swift in Sources */, 5A3C9D9322C3B1C300700DC6 /* VisionManagerNativeProtocol.swift in Sources */, 2E4EE27420F05652003431B3 /* CoreConfig.swift in Sources */, - 2EBE10E92301816600CE87CE /* Country+SyncRegion.swift in Sources */, 9D72A196BCA3C43852E39A10 /* DateFormatter.swift in Sources */, 9D72AB1C8F3CE6B6E2222E7E /* CameraVideoSource.swift in Sources */, 9D72A4A4A30F19F3EA8A7316 /* VideoSource.swift in Sources */, 9D72AB0D4BB86199CF051DC2 /* CVPixelBuffer+Helper.swift in Sources */, - 9D72A5C114A06457134C5EE5 /* RecordSynchronizer.swift in Sources */, 9D72ACF3C976A03345D44F97 /* DocumentsLocation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -899,26 +766,15 @@ files = ( 3AAB2EFB22858F28000D052E /* UIDeviceStubs.swift in Sources */, 2E1FC07B22D4BE1A009BEBFB /* RecordingPathTests.swift in Sources */, - 5A3C9D8D22C381EB00700DC6 /* MockSessionRecorder.swift in Sources */, - B51E557C215E313500650DDE /* RecordSynchronizerTests.swift in Sources */, 5A3E42C922B28EAA0035A010 /* VisionManagerTests.swift in Sources */, - 2EF31C9021904BB300752D23 /* RecordCoordinatorTests.swift in Sources */, - B50CFBAD2163C91500C9C5A5 /* MockNetworkClient.swift in Sources */, 3A11FEDF2282BCD300B68E5B /* DeviceCheckerTests.swift in Sources */, 3A7676AE22D647FE00ABC483 /* DeviceChecker.swift in Sources */, 5A3C9D9122C39F5300700DC6 /* MockDataProvider.swift in Sources */, - B50CFBAF2163C98E00C9C5A5 /* MockRecordDataSource.swift in Sources */, - 2EBE10E72301674400CE87CE /* ClosureSyncDelegate.swift in Sources */, - B50CFBB12163C9AC00C9C5A5 /* MockArchiver.swift in Sources */, - B50CFBB32163C9C500C9C5A5 /* MockFileManager.swift in Sources */, - 5A3C9D8B22C37DE200700DC6 /* MockPlatform.swift in Sources */, 5A3C9D9922C50D0000700DC6 /* MockNative.swift in Sources */, - 3ACF140022A955FD00EA3255 /* RecordingQuotaTests.swift in Sources */, 2E2B828623058986001494E5 /* FileManager+Helpers.swift in Sources */, - 2EDD8709219EDC0200760C6D /* DeviceInfoProviderTests.swift in Sources */, 5A3C9D8722C37AF700700DC6 /* MockVideoSource.swift in Sources */, + 2E283BD7237FABB700920228 /* MockFrameRecorder.swift in Sources */, 5A3C9D9B22C50D7C00700DC6 /* MockSensors.swift in Sources */, - 5A3C9D8922C37CCC00700DC6 /* MockSynchronizable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift index 44465635..0a9b29bc 100644 --- a/MapboxVision/Core/Platform.swift +++ b/MapboxVision/Core/Platform.swift @@ -5,7 +5,7 @@ typealias TelemetryFileMetadata = [String: String] final class Platform: NSObject { struct Dependencies { - let recorder: VideoRecorder? + let recorder: FrameRecordable? let videoTrimmer: VideoTrimmer? let eventsManager: EventsManager let archiver: Archiver? diff --git a/MapboxVision/Helpers/Country+SyncRegion.swift b/MapboxVision/Helpers/Country+SyncRegion.swift deleted file mode 100644 index df12db26..00000000 --- a/MapboxVision/Helpers/Country+SyncRegion.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -extension Country { - var syncRegion: SyncRegion? { - switch self { - case .USA, .UK, .germany, .other: - return .other - case .china: - return .china - case .unknown: - return nil - } - } -} diff --git a/MapboxVision/Helpers/MemoryByte.swift b/MapboxVision/Helpers/MemoryByte.swift deleted file mode 100644 index f15dce0e..00000000 --- a/MapboxVision/Helpers/MemoryByte.swift +++ /dev/null @@ -1,9 +0,0 @@ -typealias MemoryByte = UInt64 - -extension MemoryByte { - private static let bytesInKByte: UInt64 = 1024 - private static let kByteInMByte: UInt64 = 1024 - - static let kByte = bytesInKByte - static let mByte = kByteInMByte * kByte -} diff --git a/MapboxVision/Helpers/TimeInterval.swift b/MapboxVision/Helpers/TimeInterval.swift deleted file mode 100644 index 96c10d79..00000000 --- a/MapboxVision/Helpers/TimeInterval.swift +++ /dev/null @@ -1,7 +0,0 @@ -extension TimeInterval { - private static let secondsInMinute: TimeInterval = 60 - private static let minutesInHour: TimeInterval = 60 - - static let minute = secondsInMinute - static let hour = secondsInMinute * minute -} diff --git a/MapboxVision/Helpers/URL.swift b/MapboxVision/Helpers/URL.swift deleted file mode 100644 index 5ee57433..00000000 --- a/MapboxVision/Helpers/URL.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -extension URL { - var isDirectory: Bool { - return (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true - } - - var subDirectories: [URL] { - guard isDirectory else { return [] } - return (try? FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).filter { $0.isDirectory }) ?? [] - } - - var creationDate: Date? { - return (try? resourceValues(forKeys: [.creationDateKey])).flatMap { $0.creationDate } - } -} - -extension Array where Element == URL { - var sortedByCreationDate: [URL] { - return self.sorted { url1, url2 in - switch (url1.creationDate, url2.creationDate) { - case (_, .none): return true - case (.none, _): return false - case let (.some(date1), .some(date2)): return date1 < date2 - } - } - } -} diff --git a/MapboxVision/Helpers/UserDefaults+DefaultValue.swift b/MapboxVision/Helpers/UserDefaults+DefaultValue.swift deleted file mode 100644 index 0e251a80..00000000 --- a/MapboxVision/Helpers/UserDefaults+DefaultValue.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -extension UserDefaults { - func setDefaultValue(_ value: Any?, forKey: String) { - if object(forKey: forKey) == nil { - setValue(value, forKey: forKey) - } - } -} diff --git a/MapboxVision/Services/DataProvider.swift b/MapboxVision/Services/DataProvider.swift index 4276141e..7522d3d6 100644 --- a/MapboxVision/Services/DataProvider.swift +++ b/MapboxVision/Services/DataProvider.swift @@ -7,39 +7,6 @@ protocol DataProvider: AnyObject { func stop() } -final class RecordedDataProvider: DataProvider { - struct Dependencies { - let recordingPath: RecordingPath - let startTime: UInt - } - - let dependencies: Dependencies - let telemetryPlayer: TelemetryPlayer - - init(dependencies: Dependencies) { - self.dependencies = dependencies - self.telemetryPlayer = TelemetryPlayer() - telemetryPlayer.read(fromFolder: dependencies.recordingPath.recordingPath) - } - - private var startTime = DispatchTime.now().uptimeMilliseconds - - func start() { - startTime = DispatchTime.now().uptimeMilliseconds - telemetryPlayer.scrollData(dependencies.startTime) - } - - func update() { - let settings = dependencies.recordingPath.settings - let frameSize = CGSize(width: settings.width, height: settings.height) - let currentTimeMS = DispatchTime.now().uptimeMilliseconds - startTime + dependencies.startTime - telemetryPlayer.setCurrentTime(currentTimeMS) - telemetryPlayer.updateData(withFrameSize: frameSize, srcSize: frameSize) - } - - func stop() {} -} - final class RealtimeDataProvider: DataProvider { struct Dependencies { let native: VisionManagerNative @@ -68,17 +35,3 @@ final class RealtimeDataProvider: DataProvider { dependencies.motionManager.stop() } } - -final class EmptyDataProvider: DataProvider { - func start() {} - - func update() {} - - func stop() {} -} - -private extension DispatchTime { - var uptimeMilliseconds: UInt { - return UInt(DispatchTime.now().uptimeNanoseconds / 1_000_000) - } -} diff --git a/MapboxVision/Services/DeviceInfoProvider.swift b/MapboxVision/Services/DeviceInfoProvider.swift deleted file mode 100644 index c1b875b7..00000000 --- a/MapboxVision/Services/DeviceInfoProvider.swift +++ /dev/null @@ -1,24 +0,0 @@ -protocol DeviceInfoProvidable { - var id: String { get } - var platformName: String { get } -} - -final class DeviceInfoProvider: DeviceInfoProvidable { - private enum Keys { - static let uniqueDeviceIdKey = "uniqueDeviceIdKey" - } - - lazy var id: String = { - let defaults = UserDefaults.standard - - if let uuid = defaults.string(forKey: Keys.uniqueDeviceIdKey) { - return uuid - } else { - let uuid = NSUUID().uuidString - defaults.set(uuid, forKey: Keys.uniqueDeviceIdKey) - return uuid - } - }() - - let platformName: String = UIDevice.current.systemName -} diff --git a/MapboxVision/Services/EventsManager.swift b/MapboxVision/Services/EventsManager.swift index 82eb1b45..93a45f42 100644 --- a/MapboxVision/Services/EventsManager.swift +++ b/MapboxVision/Services/EventsManager.swift @@ -16,16 +16,6 @@ final class EventsManager { return token }() - private let formatter: DateFormatter = { - let dateFormatter = DateFormatter() - let enUSPosixLocale = Locale(identifier: "en_US_POSIX") - dateFormatter.locale = enUSPosixLocale - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - return dateFormatter - }() - - private lazy var recordingFormatter = DateFormatter.createRecordingFormatter() - init() { let bundle = Bundle(for: type(of: self)) let name = bundle.infoDictionary!["CFBundleName"] as! String @@ -54,53 +44,5 @@ extension EventsManager: NetworkClient { manager.postMetadata([metadata], filePaths: [file], completionHandler: completion) } - func upload(file: URL, toFolder folderName: String, completion: @escaping (Error?) -> Void) { - let contentType: String - switch file.pathExtension { - case "zip": contentType = "zip" - case "mp4": contentType = "video" - case "jpg": contentType = "image" - default: - assertionFailure("EventsManager: post unsupported content type") - contentType = "" - } - - let name = file.lastPathComponent - let folder = file.deletingLastPathComponent().lastPathComponent - - let created = file.creationDate.map(formatter.string) ?? "" - - var metadata = [ - "name": name, - "fileId": folder + "/" + name, - "sessionId": folderName, - "format": file.pathExtension, - "created": created, - "type": contentType, - ] - - if contentType == "video" { - var startTime = created - var endTime = created - - let components = name.deletingPathExtension.split(separator: "-") - if - let date = recordingFormatter.date(from: folder), - let start = components[safe: 0], - let startInterval = TimeInterval(start), - let end = components[safe: 1], - let endInterval = TimeInterval(end) - { - startTime = formatter.string(from: date.addingTimeInterval(startInterval)) - endTime = formatter.string(from: date.addingTimeInterval(endInterval)) - } - - metadata["startTime"] = startTime - metadata["endTime"] = endTime - } - - manager.postMetadata([metadata], filePaths: [file.path], completionHandler: completion) - } - func cancel() {} } diff --git a/MapboxVision/Services/Network/NetworkClient.swift b/MapboxVision/Services/Network/NetworkClient.swift index 6ad5c61f..7fa69584 100644 --- a/MapboxVision/Services/Network/NetworkClient.swift +++ b/MapboxVision/Services/Network/NetworkClient.swift @@ -3,7 +3,6 @@ import MapboxVisionNative protocol NetworkClient { func set(baseURL: URL?) - func upload(file: URL, toFolder folderName: String, completion: @escaping (Error?) -> Void) func upload(file: String, metadata: TelemetryFileMetadata, completion: @escaping (Error?) -> Void) func cancel() } diff --git a/MapboxVision/Services/Recording/FileManager.swift b/MapboxVision/Services/Recording/FileManager.swift deleted file mode 100644 index 2fd8c332..00000000 --- a/MapboxVision/Services/Recording/FileManager.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -protocol FileManagerProtocol { - func contentsOfDirectory(atPath path: String) throws -> [String] - func contentsOfDirectory(at url: URL) throws -> [URL] - func fileExists(atPath path: String) -> Bool - func createFile(atPath path: String, contents: Data?) -> Bool - func fileSize(at url: URL) -> MemoryByte - func remove(item: URL) -} - -extension FileManagerProtocol { - func sizeOfDirectory(at url: URL) -> MemoryByte { - guard let contents = try? self.contentsOfDirectory(at: url) else { - return 0 - } - - return contents.map(fileSize).reduce(0, +) - } -} - -extension FileManager: FileManagerProtocol { - func createFile(atPath path: String, contents: Data?) -> Bool { - return createFile(atPath: path, contents: contents, attributes: nil) - } - - func contentsOfDirectory(at url: URL) throws -> [URL] { - return try contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) - } - - func fileSize(at url: URL) -> MemoryByte { - guard let attributes = try? attributesOfItem(atPath: url.path) else { return 0 } - return attributes[FileAttributeKey.size] as? MemoryByte ?? 0 - } - - func remove(item: URL) { - try? removeItem(at: item) - } -} diff --git a/MapboxVision/Services/Recording/FileRecorder.swift b/MapboxVision/Services/Recording/FileRecorder.swift deleted file mode 100644 index eacbf371..00000000 --- a/MapboxVision/Services/Recording/FileRecorder.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -final class FileRecorder { - private let stream: OutputStream - private var firstChunk = true - - init?(path: String) { - guard let stream = OutputStream(toFileAtPath: path, append: true) else { return nil } - self.stream = stream - stream.open() - stream.write(string: "[") - } - - func record(_ info: T) { - if firstChunk { - firstChunk = false - } else { - stream.write(string: ",") - } - - guard let encoded = try? JSONEncoder().encode(info) - else { assertionFailure("Can't encode metainfo record to json data"); return } - - guard let jsonObject = try? JSONSerialization.jsonObject(with: encoded, options: []) - else { assertionFailure("Can't convert encoded data to json object"); return } - - var error: NSError? - JSONSerialization.writeJSONObject(jsonObject, to: self.stream, options: JSONSerialization.WritingOptions.prettyPrinted, error: &error) - if let error = error { assertionFailure(error.localizedDescription); return } - } - - deinit { - stream.write(string: "]") - stream.close() - } -} - -private extension OutputStream { - func write(string: String) { - _ = string.data(using: .utf8)?.withUnsafeBytes { ptr in - self.write(ptr, maxLength: string.lengthOfBytes(using: .utf8)) - } - } -} diff --git a/MapboxVision/Services/Recording/ImageRecorder.swift b/MapboxVision/Services/Recording/ImageRecorder.swift deleted file mode 100644 index fbd07691..00000000 --- a/MapboxVision/Services/Recording/ImageRecorder.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import UIKit - -final class ImageRecorder { - func record(image: UIImage, to path: String) { - guard let data = image.jpegData(compressionQuality: 1.0) else { - assertionFailure("ERROR: Unable to obtain data representation of UIImage") - return - } - do { - try data.write(to: URL(fileURLWithPath: path)) - } catch { - assertionFailure("ERROR: Unable to save image to \(path). Error: \(error)") - } - } -} diff --git a/MapboxVision/Services/Recording/RecordCoordinator.swift b/MapboxVision/Services/Recording/RecordCoordinator.swift deleted file mode 100644 index 760d41e7..00000000 --- a/MapboxVision/Services/Recording/RecordCoordinator.swift +++ /dev/null @@ -1,361 +0,0 @@ -import AVFoundation -import Foundation -import MapboxVisionNative -import UIKit - -protocol RecordCoordinatorDelegate: AnyObject { - func recordingStarted(path: String) - func recordingStopped(recordingPath: RecordingPath) -} - -enum RecordCoordinatorError: LocalizedError { - case cantStartAlreadyRecording - case cantStartNotReady -} - -private let defaultChunkLength: Float = 5 * 60 -private let defaultChunkLimit = 3 -private let videoLogFile = "videos.json" - -final class RecordCoordinator { - private struct VideoTrimRequest { - let sourcePath: String - let destinationPath: String - let clipStart: Float - let clipEnd: Float - let log: VideoLog - } - - private struct VideoLog: Codable { - let name: String - let start: Float - let end: Float - } - - private var trimRequestCache = [Int: [VideoTrimRequest]]() - var isRecording: Bool { - return processingQueue.sync { - isRecordingInternal - } - } - private var isRecordingInternal: Bool = false - private var isReady: Bool = true - weak var delegate: RecordCoordinatorDelegate? - - private let videoRecorder: VideoBuffer - private let videoTrimmer = VideoTrimmer() - - private var jsonWriter: FileRecorder? - private var imageWriter = ImageRecorder() - private let processingQueue = DispatchQueue(label: "com.mapbox.RecordCoordinator.Processing") - - private var currentVideoSettings: VideoSettings? - private var currentReferenceTime: Float? - private var currentRecordingPath: RecordingPath? - private var currentStartTime: DispatchTime? - private var currentEndTime: DispatchTime? - private var currentVideoIsFull = false - - private var stopRecordingInBackgroundTask = UIBackgroundTaskIdentifier.invalid - - // determines if the source video is saved - var savesSourceVideo: Bool = false - - init() { - self.videoRecorder = VideoBuffer(chunkLength: defaultChunkLength, chunkLimit: defaultChunkLimit) - videoRecorder.delegate = self - } - - func startRecording(referenceTime: Float, directory: String? = nil, videoSettings: VideoSettings, onFail: @escaping () -> Void) { - processingQueue.async { [weak self] in - guard let self = self, !self.isRecordingInternal, self.isReady else { - onFail() - return - } - - self.currentVideoSettings = videoSettings - - self.isRecordingInternal = true - self.currentReferenceTime = referenceTime - self.currentVideoIsFull = self.savesSourceVideo - - let cachePath = DocumentsLocation.cache.path - self.recreateFolder(path: DocumentsLocation.currentRecording.path) - self.recreateFolder(path: cachePath) - - let basePath: DocumentsLocation = directory != nil ? .custom : .currentRecording - let recordingPath = RecordingPath(basePath: basePath, directory: directory, settings: videoSettings) - self.currentRecordingPath = recordingPath - - self.jsonWriter = FileRecorder(path: recordingPath.videosLogPath) - - self.currentStartTime = DispatchTime.now() - self.currentEndTime = nil - - self.videoRecorder.chunkLength = self.savesSourceVideo ? 0 : defaultChunkLength - self.videoRecorder.chunkLimit = self.savesSourceVideo ? 1 : defaultChunkLimit - self.videoRecorder.startRecording(to: cachePath, settings: videoSettings) - - self.delegate?.recordingStarted(path: recordingPath.recordingPath) - } - } - - func stopRecording() { - processingQueue.async { [weak self] in - guard let self = self, self.isRecordingInternal, self.isReady else { return } - - self.stopRecordingInBackgroundTask = UIApplication.shared.beginBackgroundTask() - - self.isRecordingInternal = false - self.isReady = false - self.currentEndTime = DispatchTime.now() - self.videoRecorder.stopRecording() - } - } - - func handleFrame(_ sampleBuffer: CMSampleBuffer) { - processingQueue.async { [weak self] in - guard let self = self, self.isRecordingInternal else { return } - self.videoRecorder.handleFrame(sampleBuffer) - } - } - - func makeClip(from startTime: Float, to endTime: Float) { - processingQueue.async { [weak self] in - guard - let self = self, - let referenceTime = self.currentReferenceTime - else { return } - - let relativeStart = startTime - referenceTime - let relativeEnd = endTime - referenceTime - let chunkLength = self.videoRecorder.chunkLength - - let startChunk = chunkLength == 0 ? 0 : Int(floor(relativeStart / chunkLength)) - let endChunk = chunkLength == 0 ? 0 : Int(floor(relativeEnd / chunkLength)) - - if startChunk != endChunk { - self.clipFrom(startChunk: startChunk, endChunk: endChunk, from: startTime, to: endTime, relativeStart: relativeStart, relativeEnd: relativeEnd) - } else { - self.clipFrom(chunk: startChunk, from: startTime, to: endTime, relativeStart: relativeStart, relativeEnd: relativeEnd) - } - } - } - - func saveImage(image: Image, path: String) { - processingQueue.async { [weak self] in - guard let self = self else { return } - guard let recordingPath = self.currentRecordingPath else { return } - - guard let uiImage = image.getUIImage() else { - assertionFailure("ERROR: Unable to convert image to UIImage") - return - } - let imagePath = recordingPath.imagesDirectoryPath - .appendingPathComponent(path) - .appending(".\(RecordFileType.image.fileExtension)") - - self.imageWriter.record(image: uiImage, to: imagePath) - } - } - - private func clipFrom(startChunk: Int, endChunk: Int, from startTime: Float, to endTime: Float, relativeStart: Float, relativeEnd: Float) { - guard - let referenceTime = self.currentReferenceTime, - let recordingPath = self.currentRecordingPath, - let videoSettings = self.currentVideoSettings - else { return } - let chunkLength = self.videoRecorder.chunkLength - let clipStartTime = relativeStart - Float(startChunk) * chunkLength - let clipEndTime = relativeEnd - Float(endChunk) * chunkLength - - // trim start clip - let startJointTime = Float(startChunk + 1) * chunkLength - let startSourcePath = self.chunkPath(for: startChunk, fileExtension: videoSettings.fileExtension) - let startName = recordingPath.videoClipPath(start: relativeStart, end: relativeEnd) - let startLog = VideoLog(name: startName.lastPathComponent, - start: startTime, - end: referenceTime + startJointTime) - let trimRequest = VideoTrimRequest(sourcePath: startSourcePath, - destinationPath: startName, - clipStart: clipStartTime, - clipEnd: chunkLength, - log: startLog) - self.trimClip(chunk: startChunk, request: trimRequest) - - // copy all in-between clips - for chunk in (startChunk + 1).. Void)? = nil) { - guard FileManager.default.fileExists(atPath: request.sourcePath) else { return } - guard let settings = currentVideoSettings else { return } - - let sourceURL = URL(fileURLWithPath: request.sourcePath) - let destinationURL = URL(fileURLWithPath: request.destinationPath) - videoTrimmer.trimVideo(sourceURL: sourceURL, - destinationURL: destinationURL, - from: TimeInterval(request.clipStart), - to: TimeInterval(request.clipEnd), - settings: settings) { [jsonWriter, weak self] error in - guard let self = self else { return } - if let trimError = (error as? VideoTrimmerError), case VideoTrimmerError.sourceNotExportable = trimError { - if self.trimRequestCache[chunk] != nil { - self.trimRequestCache[chunk]?.append(request) - } else { - self.trimRequestCache[chunk] = [request] - } - } - if error == nil { - self.processingQueue.async { [jsonWriter] in - jsonWriter?.record(request.log) - completion?() - } - } else { - completion?() - } - } - } - - private func copyClip(chunk: Int, clipStart: Float, clipEnd: Float) { - guard - let referenceTime = currentReferenceTime, - let recordingPath = currentRecordingPath, - let videoSettings = currentVideoSettings - else { return } - - let sourcePath = chunkPath(for: chunk, fileExtension: videoSettings.fileExtension) - let destinationPath = currentVideoIsFull - ? recordingPath.videoPath - : recordingPath.videoClipPath(start: clipStart, end: clipEnd) - - let log = VideoLog(name: destinationPath.lastPathComponent, - start: referenceTime + clipStart, - end: referenceTime + clipEnd) - - processingQueue.async { [jsonWriter] in - do { - try FileManager.default.copyItem(atPath: sourcePath, toPath: destinationPath) - jsonWriter?.record(log) - } catch { - assertionFailure("Copy failed. Error: \(error.localizedDescription)") - } - } - } - - private func chunkPath(for number: Int, fileExtension: String) -> String { - return "\(DocumentsLocation.cache.path)/\(number).\(fileExtension)" - } - - private func recreateFolder(path: String) { - let fileManager = FileManager.default - do { - if fileManager.fileExists(atPath: path) { - try fileManager.removeItem(atPath: path) - } - try fileManager.createDirectory(atPath: path, - withIntermediateDirectories: true, - attributes: nil) - } catch { - assertionFailure("Folder recreation has failed. Error: \(error.localizedDescription)") - } - } -} - -extension RecordCoordinator: VideoBufferDelegate { - func chunkCut(number: Int, finished: Bool) { - processingQueue.async { [weak self] in - guard let self = self else { return } - if self.currentVideoIsFull, let startTime = self.currentStartTime { - let clipStart = Float(number) * self.videoRecorder.chunkLength - let clipEnd: Float - - if finished, let endTime = self.currentEndTime { - let sessionDuration = Float(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000 - clipEnd = sessionDuration - clipStart - } else { - clipEnd = Float(number + 1) * self.videoRecorder.chunkLength - } - - self.copyClip(chunk: number, clipStart: clipStart, clipEnd: clipEnd) - } - - let group = DispatchGroup() - if let requests = self.trimRequestCache.removeValue(forKey: number) { - for request in requests { - group.enter() - self.trimClip(chunk: number, request: request) { - group.leave() - } - } - } - - group.notify(queue: self.processingQueue) { [weak self] in - guard let self = self else { return } - if finished { - self.recordingStopped() - } - } - } - } - - private func endBackgroundTask() { - UIApplication.shared.endBackgroundTask(stopRecordingInBackgroundTask) - stopRecordingInBackgroundTask = UIBackgroundTaskIdentifier.invalid - } -} diff --git a/MapboxVision/Services/Recording/RecordingQuota.swift b/MapboxVision/Services/Recording/RecordingQuota.swift deleted file mode 100644 index 414ec4ba..00000000 --- a/MapboxVision/Services/Recording/RecordingQuota.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation - -final class RecordingQuota { - enum Keys { - static let recordingMemoryQuotaKey = "recordingMemoryQuota" - static let lastResetTimeKey = "lastResetTimeKey" - } - - private enum RecordingQuotaError: LocalizedError { - case memoryQuotaExceeded - } - - // MARK: - Private properties - - private let memoryQuota: MemoryByte - private let refreshInterval: TimeInterval - - private var lastResetTime: Date { - get { - let defaults = UserDefaults.standard - if let time = defaults.object(forKey: Keys.lastResetTimeKey) as? Date { - return time - } else { - let time = Date() - defaults.set(time, forKey: Keys.lastResetTimeKey) - return time - } - } - set { - UserDefaults.standard.set(newValue, forKey: Keys.lastResetTimeKey) - } - } - - private var cachedCurrentQuota: MemoryByte { - get { - if let quota = UserDefaults.standard.object(forKey: Keys.recordingMemoryQuotaKey) as? MemoryByte { - return quota - } else { - let quota = memoryQuota - UserDefaults.standard.set(quota, forKey: Keys.recordingMemoryQuotaKey) - return quota - } - } - set { - UserDefaults.standard.set(newValue, forKey: Keys.recordingMemoryQuotaKey) - } - } - - // MARK: - Lifecycle - - init(memoryQuota: MemoryByte, refreshInterval: TimeInterval) { - self.memoryQuota = memoryQuota - self.refreshInterval = refreshInterval - } - - // MARK: - Functions - - func reserve(memoryToReserve: MemoryByte) throws { - var currentQuota = cachedCurrentQuota - - let now = Date() - if now.timeIntervalSince(lastResetTime) >= refreshInterval { - currentQuota = memoryQuota - lastResetTime = now - } - - guard currentQuota >= memoryToReserve else { throw RecordingQuotaError.memoryQuotaExceeded } - cachedCurrentQuota = currentQuota - memoryToReserve - } -} diff --git a/MapboxVision/Services/Recording/SessionRecorder.swift b/MapboxVision/Services/Recording/SessionRecorder.swift deleted file mode 100644 index 009b34ed..00000000 --- a/MapboxVision/Services/Recording/SessionRecorder.swift +++ /dev/null @@ -1,129 +0,0 @@ -import CoreMedia -import Foundation - -private let internalSessionInterval: TimeInterval = 5 * 60 -private let externalSessionInterval: TimeInterval = 0 - -final class SessionRecorder { - struct Dependencies { - let recorder: RecordCoordinator - let sessionManager: SessionManager - let videoSettings: VideoSettings - let getSeconds: () -> Float - let startSavingSession: (String) -> Void - let stopSavingSession: () -> Void - } - - weak var delegate: RecordCoordinatorDelegate? - var currentMode: SessionRecordingMode = .internal - - private let dependencies: Dependencies - private var hasPendingRecordingRequest = false - - init(dependencies: Dependencies) { - self.dependencies = dependencies - - dependencies.sessionManager.delegate = self - dependencies.recorder.delegate = self - } - - func start(mode: SessionRecordingMode = .internal) { - guard !dependencies.recorder.isRecording else { return } - - currentMode = mode - dependencies.recorder.savesSourceVideo = mode.savesSourceVideo - dependencies.sessionManager.startSession(interruptionInterval: mode.sessionInterval) - } - - func stop() { - guard dependencies.recorder.isRecording else { return } - - dependencies.sessionManager.stopSession() - } - - func handleFrame(_ sampleBuffer: CMSampleBuffer) { - dependencies.recorder.handleFrame(sampleBuffer) - } - - private func record() { - dependencies.recorder.startRecording(referenceTime: dependencies.getSeconds(), - directory: currentMode.path, - videoSettings: dependencies.videoSettings) { [weak self] in - self?.hasPendingRecordingRequest = true - } - } -} - -extension SessionRecorder: SessionDelegate { - func sessionStarted() { - record() - } - - func sessionStopped() { - dependencies.stopSavingSession() - dependencies.recorder.stopRecording() - } -} - -extension SessionRecorder: RecordCoordinatorDelegate { - func recordingStarted(path: String) { - delegate?.recordingStarted(path: path) - dependencies.startSavingSession(path) - } - - func recordingStopped(recordingPath: RecordingPath) { - delegate?.recordingStopped(recordingPath: recordingPath) - - if hasPendingRecordingRequest { - hasPendingRecordingRequest = false - record() - } - } -} - -extension SessionRecorder: SessionRecorderProtocol { - var isInternal: Bool { - return currentMode.isInternal - } - - var isExternal: Bool { - return currentMode.isExternal - } -} - -private extension SessionRecordingMode { - var sessionInterval: TimeInterval { - switch self { - case .internal: - return internalSessionInterval - case .external: - return externalSessionInterval - } - } - - var isInternal: Bool { - if case .internal = self { return true } - return false - } - - var isExternal: Bool { - if case .external = self { return true } - return false - } - - var savesSourceVideo: Bool { - switch self { - case .internal: - return false - case .external: - return true - } - } - - var path: String? { - if case let .external(path) = self { - return path - } - return nil - } -} diff --git a/MapboxVision/Services/Recording/VideoBuffer.swift b/MapboxVision/Services/Recording/VideoBuffer.swift deleted file mode 100644 index 5efe9a14..00000000 --- a/MapboxVision/Services/Recording/VideoBuffer.swift +++ /dev/null @@ -1,102 +0,0 @@ -import CoreMedia -import Foundation - -protocol VideoBufferDelegate: AnyObject { - func chunkCut(number: Int, finished: Bool) -} - -final class VideoBuffer { - private(set) var isRecording: Bool = false - weak var delegate: VideoBufferDelegate? - - // make it 0 to prevent cutting video in chunks - var chunkLength: Float - - var chunkLimit: Int - - private let recorder = VideoRecorder() - private var chunkCounter: Int = 0 - private var currentTimer: Timer? - private var currentBasePath: String? - - private var settings: VideoSettings? - - init(chunkLength: Float, chunkLimit: Int) { - self.chunkLength = chunkLength - self.chunkLimit = chunkLimit - } - - func startRecording(to path: String, settings: VideoSettings) { - self.settings = settings - isRecording = true - chunkCounter = 0 - currentBasePath = path - if chunkLength > 0 { - currentTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(chunkLength), repeats: true) { [weak self] _ in - self?.cutChunk(true) - } - } - startChunk() - } - - func stopRecording() { - currentTimer?.invalidate() - isRecording = false - cutChunk(false) - settings = nil - } - - func handleFrame(_ sampleBuffer: CMSampleBuffer) { - guard isRecording, recorder.isRecording else { return } - - recorder.handleFrame(sampleBuffer) { [weak self] result in - guard let self = self, self.isRecording, self.recorder.isRecording else { return } - - switch result { - case .value: - break - case .error(.notReadyForData): - break - case .error(.notRecording): - assertionFailure("Video buffer is recording while video is not recording") - fallthrough - case .error(.recordingFailed): - self.cutChunk(false) - } - } - } - - private func startChunk() { - if isRecording, let basePath = currentBasePath, let settings = settings { - cleanupBuffer() - recorder.startRecording(to: "\(basePath)/\(chunkCounter).\(settings.fileExtension)", settings: settings) - } - } - - private func cutChunk(_ shouldContinue: Bool) { - let isCurrentlyRecording = isRecording - - recorder.stopRecording { [weak self] in - guard let self = self else { return } - DispatchQueue.main.async { - self.delegate?.chunkCut(number: self.chunkCounter, finished: !isCurrentlyRecording) - if shouldContinue { - self.chunkCounter += 1 - self.startChunk() - } - } - } - } - - private func cleanupBuffer() { - guard let basePath = currentBasePath else { return } - - let fileManager = FileManager.default - if let contents = try? fileManager.contentsOfDirectory(atPath: basePath), - contents.count >= chunkLimit { - contents.sorted().prefix(contents.count - chunkLimit).forEach { - try? fileManager.removeItem(atPath: "\(basePath)/\($0)") - } - } - } -} diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index 1d9249e6..8af23677 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -19,7 +19,7 @@ private extension VideoSettings { } protocol FrameRecordable { - func startRecording(filePath: String, settings: VideoSettings) + func startRecording(to path: String, settings: VideoSettings) func stopRecording(completion: (() -> Void)?) func handle(frame: CMSampleBuffer) } @@ -83,7 +83,7 @@ final class VideoRecorder { sem.signal() } sem.wait() - + cleanup() } } @@ -139,10 +139,6 @@ final class VideoRecorder { } extension VideoRecorder: FrameRecordable { - func startRecording(filePath: String, settings: VideoSettings) { - startRecording(to: filePath, settings: settings) - } - func handle(frame: CMSampleBuffer) { handleFrame(frame) { _ in } } diff --git a/MapboxVision/Services/Recording/VideoTrimmer.swift b/MapboxVision/Services/Recording/VideoTrimmer.swift index dd59c63f..669cc5b8 100644 --- a/MapboxVision/Services/Recording/VideoTrimmer.swift +++ b/MapboxVision/Services/Recording/VideoTrimmer.swift @@ -10,57 +10,6 @@ enum VideoTrimmerError: LocalizedError { final class VideoTrimmer { typealias TrimCompletion = (Error?) -> Void - typealias TrimPoints = (startTime: CMTime, endTime: CMTime) - - func trimVideo(sourceURL: URL, destinationURL: URL, from start: TimeInterval, to end: TimeInterval, settings: VideoSettings, completion: TrimCompletion?) { - print("log_t: trim source: \(sourceURL.path), dest: \(destinationURL.path) from \(start) to \(end)") - let startTime = CMTime(seconds: start, preferredTimescale: timeScale) - let endTime = CMTime(seconds: end, preferredTimescale: timeScale) - - let options = [ - AVURLAssetPreferPreciseDurationAndTimingKey: true, - ] - - let asset = AVURLAsset(url: sourceURL, options: options) - guard asset.isExportable else { - completion?(VideoTrimmerError.sourceNotExportable) - return - } - - let preferredPreset = AVAssetExportPresetPassthrough - - let composition = AVMutableComposition() - guard - let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID()) - else { - assertionFailure("Unable to add video track to composition \(composition).") - return - } - - guard let videoAssetTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first else { - assertionFailure("Unable to obtain video track from asset \(asset).") - return - } - - let durationOfCurrentSlice = CMTimeSubtract(endTime, startTime) - let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTime, duration: durationOfCurrentSlice) - - do { - try videoTrack.insertTimeRange(timeRangeForCurrentSlice, of: videoAssetTrack, at: CMTime()) - } catch { - completion?(error) - } - - guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else { return } - - exportSession.outputURL = destinationURL - exportSession.outputFileType = settings.fileType - exportSession.shouldOptimizeForNetworkUse = true - - exportSession.exportAsynchronously { - completion?(exportSession.error) - } - } func trimVideo(source: String, clip: VideoClip, completion: @escaping TrimCompletion) { let sourceURL = URL(fileURLWithPath: source) diff --git a/MapboxVision/Services/SessionManager.swift b/MapboxVision/Services/SessionManager.swift deleted file mode 100644 index 1c814e80..00000000 --- a/MapboxVision/Services/SessionManager.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -protocol SessionDelegate: AnyObject { - func sessionStarted() - func sessionStopped() -} - -final class SessionManager { - weak var delegate: SessionDelegate? - - private var notificationObservers = [Any]() - private var interruptionInterval: TimeInterval = 0 - private var interruptionTimer: Timer? - - private var isStarted = false - - func startSession(interruptionInterval: TimeInterval) { - guard !isStarted else { return } - isStarted.toggle() - - let observer = NotificationCenter.default.addObserver(forName: UIApplication.willTerminateNotification, - object: nil, - queue: .main) { [weak self] _ in - self?.stopSession() - } - notificationObservers.append(observer) - - if interruptionInterval > 0 { - interruptionTimer = Timer.scheduledTimer(withTimeInterval: interruptionInterval, repeats: true) { [weak self] _ in - self?.stopInterval() - self?.startInterval() - } - } - startInterval() - } - - func stopSession() { - guard isStarted else { return } - isStarted.toggle() - - notificationObservers.forEach(NotificationCenter.default.removeObserver) - interruptionTimer?.invalidate() - stopInterval() - } - - private func startInterval() { - delegate?.sessionStarted() - } - - private func stopInterval() { - delegate?.sessionStopped() - } -} diff --git a/MapboxVision/Services/Storage/RecordDataSource.swift b/MapboxVision/Services/Storage/RecordDataSource.swift deleted file mode 100644 index 79461ac8..00000000 --- a/MapboxVision/Services/Storage/RecordDataSource.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -protocol RecordDataSource { - var baseURL: URL { get } - var recordDirectories: [URL] { get } -} - -extension RecordDataSource { - var recordDirectories: [URL] { - return baseURL.subDirectories - } -} - -final class SyncRecordDataSource: RecordDataSource { - private let region: SyncRegion - - init(region: SyncRegion) { - self.region = region - } - - var baseURL: URL { - return URL(fileURLWithPath: DocumentsLocation.recordings(region).path, isDirectory: true) - } -} diff --git a/MapboxVision/Services/Storage/RecordFileType.swift b/MapboxVision/Services/Storage/RecordFileType.swift deleted file mode 100644 index 0663c9b9..00000000 --- a/MapboxVision/Services/Storage/RecordFileType.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -enum RecordFileType: Int { - case video - case bin - case json - case archive - case image - - var fileExtension: String { - switch self { - case .video: - return "mp4" - case .bin: - return "bin" - case .json: - return "json" - case .archive: - return "zip" - case .image: - return "jpg" - } - } -} diff --git a/MapboxVision/Services/Sync/ManagedSynchronizer.swift b/MapboxVision/Services/Sync/ManagedSynchronizer.swift deleted file mode 100644 index cfd7c87a..00000000 --- a/MapboxVision/Services/Sync/ManagedSynchronizer.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation - -final class ManagedSynchronizer: Synchronizable { - struct Dependencies { - let base: Synchronizable - let reachability: Reachability - } - - weak var delegate: SyncDelegate? - - private let dependencies: Dependencies - - private var isExternallyAllowed = false - private var backgroundTask = UIBackgroundTaskIdentifier.invalid - - // MARK: Initialization - - init(dependencies: Dependencies) { - self.dependencies = dependencies - - self.delegate = dependencies.base.delegate - dependencies.base.delegate = self - - dependencies.reachability.whenReachable = { [weak self] _ in - self?.continueSync() - } - dependencies.reachability.whenUnreachable = { [weak self] _ in - self?.pauseSync() - } - - try? dependencies.reachability.startNotifier() - } - - // MARK: Public - - func set(dataSource: RecordDataSource, baseURL: URL?) { - dependencies.base.set(dataSource: dataSource, baseURL: baseURL) - } - - func sync() { - isExternallyAllowed = true - continueSync() - } - - func stopSync() { - isExternallyAllowed = false - pauseSync() - } - - // MARK: Private - - private var isSyncAllowed: Bool { - return isExternallyAllowed && dependencies.reachability.connection != .unavailable - } - - private func continueSync() { - guard isSyncAllowed else { - pauseSync() - return - } - dependencies.base.sync() - } - - private func pauseSync() { - dependencies.base.stopSync() - } -} - -extension ManagedSynchronizer: SyncDelegate { - func syncStarted() { - backgroundTask = UIApplication.shared.beginBackgroundTask() - } - - func syncStopped() { - UIApplication.shared.endBackgroundTask(backgroundTask) - } -} diff --git a/MapboxVision/Services/Sync/RecordSynchronizer.swift b/MapboxVision/Services/Sync/RecordSynchronizer.swift deleted file mode 100644 index 1bf8617f..00000000 --- a/MapboxVision/Services/Sync/RecordSynchronizer.swift +++ /dev/null @@ -1,270 +0,0 @@ -import Foundation - -private let memoryLimit: MemoryByte = 300 * .mByte -private let networkingMemoryLimit: MemoryByte = 30 * .mByte -private let updatingInterval = 1 * .hour - -final class RecordSynchronizer: Synchronizable { - enum RecordSynchronizerError: LocalizedError { - case syncFileCreationFail(URL) - } - - struct Dependencies { - let networkClient: NetworkClient - let deviceInfo: DeviceInfoProvidable - let archiver: Archiver - let fileManager: FileManagerProtocol - } - - private enum State { - case idle - case syncing - case stopping - } - - weak var delegate: SyncDelegate? - - private let dependencies: Dependencies - private var dataSource: RecordDataSource? - private let queue = DispatchQueue(label: "com.mapbox.RecordSynchronizer") - private let syncFileName = ".synced" - private let telemetryFileName = "telemetry" - private let imagesSubpath = "images" - private let imagesFileName = "images" - private let quota = RecordingQuota(memoryQuota: networkingMemoryLimit, refreshInterval: updatingInterval) - - private var state: State = .idle { - didSet { - guard let delegate = delegate else { return } - switch state { - case .idle: - DispatchQueue.main.async(execute: delegate.syncStopped) - case .syncing: - DispatchQueue.main.async(execute: delegate.syncStarted) - case .stopping: - break - } - } - } - - private var hasPendingRequest: Bool = false - - init(_ dependencies: Dependencies) { - self.dependencies = dependencies - } - - func set(dataSource: RecordDataSource, baseURL: URL?) { - dependencies.networkClient.set(baseURL: baseURL) - self.dataSource = dataSource - } - - func sync() { - queue.async { [weak self] in - guard let self = self else { return } - - if self.state != .idle { - self.hasPendingRequest = true - return - } - - self.executeSync() - } - } - - private func executeSync() { - hasPendingRequest = false - state = .syncing - - guard let directories = dataSource?.recordDirectories else { return } - clean(directories) - - uploadTelemetry(directories) { [weak self] in - guard let self = self, self.canContinue() else { return } - - self.uploadImages(directories) { [weak self] in - guard let self = self, self.canContinue() else { return } - - self.uploadVideos(directories) { [weak self] in - guard let self = self, self.canContinue() else { return } - - self.state = .idle - } - } - } - } - - private func isMarkAsSynced(url: URL) -> Bool { - guard let content = try? dependencies.fileManager.contentsOfDirectory(atPath: url.path) else { - return false - } - return content.contains(syncFileName) - } - - private func getFiles(_ url: URL, types: [RecordFileType]) throws -> [URL] { - let extensions = types.map { $0.fileExtension } - let files = try dependencies.fileManager.contentsOfDirectory(at: url) - .filter { extensions.contains($0.pathExtension) } - - return files - } - - private func uploadTelemetry(_ directories: [URL], completion: @escaping () -> Void) { - uploadArchivedFiles(directories, types: [.bin, .json], archiveName: telemetryFileName, eachDirectoryCompletion: { [weak self] dir, remoteDir in - do { - try self?.markAsSynced(dir: dir, remoteDir: remoteDir) - } catch { - print(error) - } - }, completion: completion) - } - - private func uploadImages(_ directories: [URL], completion: @escaping () -> Void) { - uploadArchivedFiles(directories, types: [.image], subPath: imagesSubpath, archiveName: imagesFileName, completion: completion) - } - - private func uploadArchivedFiles( - _ directories: [URL], - types: [RecordFileType], - subPath: String? = nil, - archiveName: String, - eachDirectoryCompletion: ((_ dir: URL, _ remoteDir: String) -> Void)? = nil, - completion: @escaping () -> Void - ) { - let group = DispatchGroup() - - for dir in directories { - group.enter() - - let destination = dir.appendingPathComponent(archiveName).appendingPathExtension(RecordFileType.archive.fileExtension) - - do { - if !dependencies.fileManager.fileExists(atPath: destination.path) { - var sourceDir = dir - if let subPath = subPath { - sourceDir.appendPathComponent(subPath, isDirectory: true) - } - - let files = try getFiles(sourceDir, types: types) - guard !files.isEmpty else { - group.leave() - continue - } - - try dependencies.archiver.archive(files, destination: destination) - files.forEach(dependencies.fileManager.remove) - } - - try self.quota.reserve(memoryToReserve: dependencies.fileManager.fileSize(at: destination)) - } catch { - print("Directory \(dir) failed to archive. Error: \(error.localizedDescription)") - group.leave() - continue - } - - let remoteDir = createRemoteDirName(dir) - - dependencies.networkClient.upload(file: destination, toFolder: remoteDir) { [weak self] error in - if let error = error { - print(error) - } else { - self?.dependencies.fileManager.remove(item: destination) - eachDirectoryCompletion?(dir, remoteDir) - } - group.leave() - } - } - - group.notify(queue: queue, execute: completion) - } - - private func uploadVideos(_ directories: [URL], completion: @escaping () -> Void) { - let group = DispatchGroup() - - let fileSize = dependencies.fileManager.fileSize - let sorted = directories - .flatMap { (try? self.getFiles($0, types: [.video])) ?? [] } - .sorted { fileSize($0) < fileSize($1) } - - for file in sorted { - group.enter() - - do { - try quota.reserve(memoryToReserve: fileSize(file)) - } catch { - print("Quota reservation error: \(error.localizedDescription)") - group.leave() - continue - } - - let remoteDir = createRemoteDirName(file.deletingLastPathComponent()) - - dependencies.networkClient.upload(file: file, toFolder: remoteDir) { [weak self] error in - if let error = error { - print(error) - } else { - self?.dependencies.fileManager.remove(item: file) - } - group.leave() - } - } - - group.notify(queue: queue, execute: completion) - } - - private func clean(_ directories: [URL]) { - directories - .sortedByCreationDate - .filter(isMarkAsSynced) - .reduce(([URL](), MemoryByte(0))) { base, url in - let dirSize = dependencies.fileManager.sizeOfDirectory(at: url) - - let totalDirSize = base.1 + dirSize - if totalDirSize > memoryLimit || dirSize == 0 { - return (base.0 + [url], totalDirSize) - } else { - return (base.0, totalDirSize) - } - }.0 - .forEach(dependencies.fileManager.remove) - } - - private func markAsSynced(dir: URL, remoteDir: String) throws { - guard createSyncFile(in: dir) != nil else { - throw RecordSynchronizerError.syncFileCreationFail(dir) - } - } - - private func createRemoteDirName(_ dir: URL) -> String { - return Path([ - dir.lastPathComponent, - Locale.current.identifier, - dependencies.deviceInfo.id, - dependencies.deviceInfo.platformName, - ]).components.joined(separator: "_") - } - - private func createSyncFile(in url: URL) -> URL? { - let syncFilePath = url.appendingPathComponent(syncFileName).path - guard dependencies.fileManager.createFile(atPath: syncFilePath, contents: nil) else { - return nil - } - return URL(fileURLWithPath: syncFilePath, relativeTo: url) - } - - private func canContinue() -> Bool { - if state == .stopping { - if hasPendingRequest { - executeSync() - } else { - state = .idle - } - return false - } - return true - } - - func stopSync() { - state = state == .syncing ? .stopping : .idle - dependencies.networkClient.cancel() - } -} diff --git a/MapboxVision/Services/Sync/Synchronizable.swift b/MapboxVision/Services/Sync/Synchronizable.swift deleted file mode 100644 index 5cbdaf9e..00000000 --- a/MapboxVision/Services/Sync/Synchronizable.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -protocol SyncDelegate: AnyObject { - func syncStarted() - func syncStopped() -} - -protocol Synchronizable: AnyObject { - var delegate: SyncDelegate? { get set } - - func set(dataSource: RecordDataSource, baseURL: URL?) - - func sync() - func stopSync() -} diff --git a/MapboxVision/VisionManager/ManagerDependencies.swift b/MapboxVision/VisionManager/ManagerDependencies.swift index aa4a529c..b9835e0a 100644 --- a/MapboxVision/VisionManager/ManagerDependencies.swift +++ b/MapboxVision/VisionManager/ManagerDependencies.swift @@ -9,51 +9,21 @@ struct BaseDependencies { struct VisionDependencies { let native: VisionManagerNativeProtocol -// let synchronizer: Synchronizable -// let recorder: SessionRecorderProtocol let recorder: FrameRecordable let dataProvider: DataProvider - let deviceInfo: DeviceInfoProvidable static func `default`() -> VisionDependencies { -// guard let reachability = Reachability() else { -// fatalError("Reachability failed to initialize") -// } - - let eventsManager = EventsManager() - let deviceInfo = DeviceInfoProvider() - - let recordArchiver = RecordArchiver() - let recorder = VideoRecorder() -// let recordSyncDependencies = RecordSynchronizer.Dependencies( -// networkClient: eventsManager, -// deviceInfo: deviceInfo, -// archiver: recordArchiver, -// fileManager: FileManager.default -// ) -// let recordSynchronizer = RecordSynchronizer(recordSyncDependencies) - -// let recordCoordinator = RecordCoordinator() let platform = Platform(dependencies: Platform.Dependencies( recorder: recorder, videoTrimmer: VideoTrimmer(), - eventsManager: eventsManager, - archiver: recordArchiver + eventsManager: EventsManager(), + archiver: RecordArchiver() )) let native = VisionManagerNative.create(withPlatform: platform) -// let recorder = SessionRecorder(dependencies: SessionRecorder.Dependencies( -// recorder: recordCoordinator, -// sessionManager: SessionManager(), -// videoSettings: visionVideoSettings, -// getSeconds: native.getSeconds, -// startSavingSession: native.startSavingSession, -// stopSavingSession: native.stopSavingSession -// )) - let dataProvider = RealtimeDataProvider(dependencies: RealtimeDataProvider.Dependencies( native: native, motionManager: MotionManager(), @@ -61,10 +31,8 @@ struct VisionDependencies { )) return VisionDependencies(native: native, -// synchronizer: synchronizer, recorder: recorder, - dataProvider: dataProvider, - deviceInfo: deviceInfo) + dataProvider: dataProvider) } } diff --git a/MapboxVision/VisionManager/OperationMode.swift b/MapboxVision/VisionManager/OperationMode.swift deleted file mode 100644 index aaad08cf..00000000 --- a/MapboxVision/VisionManager/OperationMode.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation - -/** - Operation mode determines whether `VisionManager` works normally or focuses just on gathering data. - */ -public enum OperationMode { - /// Utilizes machine learning models and uploads gathered telemetry - case normal - /// Turns off machine learning inference, saves source videos, stores telemetry locally - case dataRecording - - var usesSegmentation: Bool { - switch self { - case .normal: - return true - case .dataRecording: - return false - } - } - - var usesDetection: Bool { - switch self { - case .normal: - return true - case .dataRecording: - return false - } - } - - var savesSourceVideo: Bool { - switch self { - case .normal: - return false - case .dataRecording: - return true - } - } - - var isSyncEnabled: Bool { - switch self { - case .normal: - return true - case .dataRecording: - return false - } - } - - var sessionInterval: TimeInterval { - switch self { - case .normal: - return 5 * 60 - case .dataRecording: - return 30 * 60 - } - } - - var videoSettings: VideoSettings { - switch self { - case .normal: - return VideoSettings.lowQuality - case .dataRecording: - return VideoSettings.highQuality - } - } -} diff --git a/MapboxVision/VisionManager/SessionRecorderProtocol.swift b/MapboxVision/VisionManager/SessionRecorderProtocol.swift deleted file mode 100644 index 0922cea1..00000000 --- a/MapboxVision/VisionManager/SessionRecorderProtocol.swift +++ /dev/null @@ -1,19 +0,0 @@ -import CoreMedia -import Foundation - -protocol SessionRecorderProtocol: AnyObject { - func stop() - func start(mode: SessionRecordingMode) - - func handleFrame(_ sampleBuffer: CMSampleBuffer) - - var isInternal: Bool { get } - var isExternal: Bool { get } - - var delegate: RecordCoordinatorDelegate? { get set } -} - -enum SessionRecordingMode: Equatable { - case `internal` - case external(path: String) -} diff --git a/MapboxVision/VisionManager/VisionManager.swift b/MapboxVision/VisionManager/VisionManager.swift index 4921a4d5..719c3234 100644 --- a/MapboxVision/VisionManager/VisionManager.swift +++ b/MapboxVision/VisionManager/VisionManager.swift @@ -186,7 +186,6 @@ public final class VisionManager: BaseVisionManager { state = .initialized(videoSource: videoSource) -// dependencies.recorder.delegate = self dependencies.native.videoSource = VideoSourceObserverProxy(withVideoSource: videoSource) } @@ -194,10 +193,6 @@ public final class VisionManager: BaseVisionManager { destroy() } -// private var isSyncAllowed: Bool { -// return currentCountry.syncRegion != nil -// } - private func startVideoStream() { guard case let .started(videoSource) = state else { return } videoSource.add(observer: self) @@ -212,56 +207,14 @@ public final class VisionManager: BaseVisionManager { dependencies.dataProvider.start() startVideoStream() dependencies.native.start() - -// dependencies.recorder.start(mode: .internal) } private func pause() { dependencies.dataProvider.stop() stopVideoStream() dependencies.native.stop() - -// dependencies.recorder.stop() } -// private func configureRecording(oldCountry: Country, newCountry: Country) { -// guard -// state.isStarted, -// dependencies.recorder.isInternal, -// let oldRegion = oldCountry.syncRegion, -// oldRegion != newCountry.syncRegion -// else { return } -// -// if let path = currentRecordingPath { -// recordingToCountryCache[path] = oldCountry -// } -// dependencies.recorder.stop() -// dependencies.recorder.start(mode: .internal) -// } - -// private func configureSync(oldCountry: Country, newCountry: Country) { -// if newCountry.syncRegion == nil { -// dependencies.synchronizer.stopSync() -// return -// } -// -// guard -// let newRegion = newCountry.syncRegion, -// oldCountry.syncRegion != newRegion -// else { return } -// -// let dataSource = SyncRecordDataSource(region: newRegion) -// dependencies.synchronizer.stopSync() -// dependencies.synchronizer.set(dataSource: dataSource, baseURL: newRegion.baseURL) -// dependencies.synchronizer.sync() -// } - -// private func trySync() { -// if isSyncAllowed { -// dependencies.synchronizer.sync() -// } -// } - override func prepareForBackground() { guard state.isStarted else { return } isStoppedForBackground = true @@ -273,16 +226,6 @@ public final class VisionManager: BaseVisionManager { isStoppedForBackground = false resume() } - -// override public func onCountryUpdated(_ country: Country) { -// let oldCountry = currentCountry -// currentCountry = country -// -// configureRecording(oldCountry: oldCountry, newCountry: country) -// configureSync(oldCountry: oldCountry, newCountry: country) -// -// super.onCountryUpdated(country) -// } } /// :nodoc: @@ -305,30 +248,3 @@ extension VisionManager: VideoSourceObserver { dependencies.native.sensors.setCameraParameters(cameraParameters) } } - -//extension VisionManager: RecordCoordinatorDelegate { -// func recordingStarted(path: String) { -// currentRecordingPath = path.lastPathComponent -// } -// -// func recordingStopped(recordingPath: RecordingPath) { -// currentRecordingPath = nil -// -// let country = recordingToCountryCache.removeValue(forKey: recordingPath.recordingPath.lastPathComponent) -// ?? currentCountry -// -// try? handle(recordingPath: recordingPath, country: country) -//// trySync() -// } -// -// private func handle(recordingPath: RecordingPath, country: Country) throws { -// guard recordingPath.basePath != .custom else { return } -// -// guard let syncRegion = country.syncRegion else { -// try recordingPath.delete() -// return -// } -// -// try recordingPath.move(to: .recordings(syncRegion)) -// } -//} diff --git a/MapboxVisionTests/DeviceCheckerTests.swift b/MapboxVisionTests/DeviceCheckerTests.swift index c8d0a412..103ac4e2 100644 --- a/MapboxVisionTests/DeviceCheckerTests.swift +++ b/MapboxVisionTests/DeviceCheckerTests.swift @@ -89,6 +89,18 @@ class DeviceCheckerTests: XCTestCase { // Given iPhone XR // When // Then XCTAssertTrue(UIDeviceIphoneXRStub().isHighPerformance) + + // Given iPhone 11 + // When // Then + XCTAssertTrue(UIDeviceIphone11Stub().isHighPerformance) + + // Given iPhone 11 Pro + // When // Then + XCTAssertTrue(UIDeviceIphone11ProStub().isHighPerformance) + + // Given iPhone 11 Pro Max + // When // Then + XCTAssertTrue(UIDeviceIphone11ProMaxStub().isHighPerformance) } func testIsHighPerformanceDeviceReturnsFalseOnIphoneNextGeneration() { diff --git a/MapboxVisionTests/DeviceInfoProviderTests.swift b/MapboxVisionTests/DeviceInfoProviderTests.swift deleted file mode 100644 index df6c23f4..00000000 --- a/MapboxVisionTests/DeviceInfoProviderTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -@testable import MapboxVision -import XCTest - -final class DeviceInfoProviderTests: XCTestCase { - func testIdReturnsPersistentUUIDBetweenCalls() { - // Given - let deviceInfoProvider = DeviceInfoProvider() - let initialUUID = deviceInfoProvider.id - - // When - let nextUUID = deviceInfoProvider.id - - // Then - XCTAssertEqual(initialUUID, nextUUID) - } - - func testIdReturnsPersistentUUIDBetweenSessions() { - // Given - let firstDeviceInfoProvider = DeviceInfoProvider() - let initialUUID = firstDeviceInfoProvider.id - - // When - let secondDeviceInfoProvider = DeviceInfoProvider() - let nextUUID = secondDeviceInfoProvider.id - - // Then - XCTAssertEqual(initialUUID, nextUUID) - } - - func testPlatfromNameReturnsIOSOnIOSDevices() { - // Given - let deviceInfoProvider = DeviceInfoProvider() - let expectedPlatformName = "iOS" - - // When - let platformName = deviceInfoProvider.platformName - - // Then - XCTAssertEqual(platformName, expectedPlatformName) - } -} diff --git a/MapboxVisionTests/Helpers/ClosureSyncDelegate.swift b/MapboxVisionTests/Helpers/ClosureSyncDelegate.swift deleted file mode 100644 index 35c1213e..00000000 --- a/MapboxVisionTests/Helpers/ClosureSyncDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -@testable import MapboxVision - -final class ClosureSyncDelegate: SyncDelegate { - var onSyncStarted: (() -> Void)? - var onSyncStopped: (() -> Void)? - - func syncStarted() { - onSyncStarted?() - } - - func syncStopped() { - onSyncStopped?() - } -} diff --git a/MapboxVisionTests/Mocks/MockArchiver.swift b/MapboxVisionTests/Mocks/MockArchiver.swift deleted file mode 100644 index 3d430ec8..00000000 --- a/MapboxVisionTests/Mocks/MockArchiver.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -@testable import MapboxVision - -final class MockArchiver: Archiver { - var archives: [URL: [URL]] = [:] - - func archive(_ files: [URL], destination: URL) throws { - archives[destination] = files - } -} diff --git a/MapboxVisionTests/Mocks/MockFileManager.swift b/MapboxVisionTests/Mocks/MockFileManager.swift deleted file mode 100644 index ce9ad954..00000000 --- a/MapboxVisionTests/Mocks/MockFileManager.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -@testable import MapboxVision - -final class MockFileManager: FileManagerProtocol { - struct File { - let url: URL - let size: MemoryByte - } - - var data: [File] = [] - - var urls: [URL] { - return data.map { $0.url } - } - - func contentsOfDirectory(atPath path: String) throws -> [String] { - return try contentsOfDirectory(at: URL(fileURLWithPath: path, isDirectory: true)).map { $0.lastPathComponent } - } - - func contentsOfDirectory(at url: URL) throws -> [URL] { - return data.filter { $0.url.deletingLastPathComponent() == url }.map { $0.url } - } - - func fileExists(atPath path: String) -> Bool { - let url = URL(fileURLWithPath: path) - return data.contains { $0.url == url } - } - - func createFile(atPath path: String, contents: Data?) -> Bool { - let fileUrl = URL(fileURLWithPath: path) - let file = File(url: fileUrl, size: fileSize(at: fileUrl)) - data.append(file) - return true - } - - func fileSize(at url: URL) -> MemoryByte { - return data.first { $0.url == url }?.size ?? 0 - } - - func remove(item: URL) { - data.removeAll { $0.url == item } - } -} diff --git a/MapboxVisionTests/Mocks/MockFrameRecorder.swift b/MapboxVisionTests/Mocks/MockFrameRecorder.swift new file mode 100644 index 00000000..798b336a --- /dev/null +++ b/MapboxVisionTests/Mocks/MockFrameRecorder.swift @@ -0,0 +1,24 @@ +import Foundation +@testable import MapboxVision + +class MockFrameRecorder: NSObject, FrameRecordable { + enum Action: Equatable { + case startRecording + case stopRecording + case handleFrame + } + + private(set) var actionLog: [Action] = [] + + func startRecording(to path: String, settings: VideoSettings) { + actionLog.append(.startRecording) + } + + func stopRecording(completion: (() -> Void)?) { + actionLog.append(.stopRecording) + } + + func handle(frame: CMSampleBuffer) { + actionLog.append(.handleFrame) + } +} diff --git a/MapboxVisionTests/Mocks/MockNative.swift b/MapboxVisionTests/Mocks/MockNative.swift index ff9f9ab5..cb69194e 100644 --- a/MapboxVisionTests/Mocks/MockNative.swift +++ b/MapboxVisionTests/Mocks/MockNative.swift @@ -28,6 +28,10 @@ class MockNative: VisionManagerNativeProtocol { func stop() {} + func startRecording(to path: String) {} + + func stopRecording() {} + func destroy() { isDestroyed = true } diff --git a/MapboxVisionTests/Mocks/MockNetworkClient.swift b/MapboxVisionTests/Mocks/MockNetworkClient.swift deleted file mode 100644 index d9e52819..00000000 --- a/MapboxVisionTests/Mocks/MockNetworkClient.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -@testable import MapboxVision - -final class MockNetworkClient: NetworkClient { - var baseURL: URL? - var error: Error? - var uploaded: [URL: String] = [:] - - func set(baseURL: URL?) { - self.baseURL = baseURL - } - - func upload(file: URL, toFolder folderName: String, completion: @escaping (Error?) -> Void) { - uploaded[file] = folderName - completion(error) - } - - func cancel() {} -} diff --git a/MapboxVisionTests/Mocks/MockPlatform.swift b/MapboxVisionTests/Mocks/MockPlatform.swift deleted file mode 100644 index 1d45b42a..00000000 --- a/MapboxVisionTests/Mocks/MockPlatform.swift +++ /dev/null @@ -1,9 +0,0 @@ -@testable import MapboxVision - -class MockPlatform: NSObject, PlatformInterface { - func makeVideoClip(_ startTime: Float, end endTime: Float) {} - - func sendTelemetry(_ name: String, entries: [TelemetryEntry]) {} - - func save(image: Image, path: String) {} -} diff --git a/MapboxVisionTests/Mocks/MockRecordDataSource.swift b/MapboxVisionTests/Mocks/MockRecordDataSource.swift deleted file mode 100644 index d83bf0f7..00000000 --- a/MapboxVisionTests/Mocks/MockRecordDataSource.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -@testable import MapboxVision - -final class MockRecordDataSource: RecordDataSource { - var baseURL: URL { - return URL(fileURLWithPath: "") - } - - var recordDirectories: [URL] = [] -} diff --git a/MapboxVisionTests/Mocks/MockSessionRecorder.swift b/MapboxVisionTests/Mocks/MockSessionRecorder.swift deleted file mode 100644 index 8d837972..00000000 --- a/MapboxVisionTests/Mocks/MockSessionRecorder.swift +++ /dev/null @@ -1,52 +0,0 @@ -import CoreMedia -import Foundation -@testable import MapboxVision - -class MockSessionRecorder: SessionRecorderProtocol { - enum Action: Equatable { - case stop - case startInternal - case startExternal(withPath: String) - case handleFrame(frame: CMSampleBuffer) - } - - private(set) var actionsLog: [Action] = [] - private var currentRecoring: RecordingPath? - - func stop() { - isInternal = true - actionsLog.append(.stop) - - if let recording = currentRecoring { - delegate?.recordingStopped(recordingPath: recording) - } - } - - func start(mode: SessionRecordingMode) { - isInternal = mode == .internal - - let recordingPath: RecordingPath - if case let .external(path) = mode { - actionsLog.append(.startExternal(withPath: path)) - recordingPath = RecordingPath(basePath: .custom, directory: path, settings: .highQuality) - } else { - actionsLog.append(.startInternal) - recordingPath = RecordingPath(basePath: .currentRecording, settings: .highQuality) - } - - currentRecoring = recordingPath - delegate?.recordingStarted(path: recordingPath.recordingPath) - } - - func handleFrame(_ frame: CMSampleBuffer) { - actionsLog.append(.handleFrame(frame: frame)) - } - - private(set) var isInternal: Bool = true - - var isExternal: Bool { - return !isInternal - } - - weak var delegate: RecordCoordinatorDelegate? -} diff --git a/MapboxVisionTests/Mocks/MockSynchronizable.swift b/MapboxVisionTests/Mocks/MockSynchronizable.swift deleted file mode 100644 index 30e75933..00000000 --- a/MapboxVisionTests/Mocks/MockSynchronizable.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -@testable import MapboxVision - -class MockSynchronizable: Synchronizable { - enum Action: Equatable { - case sync - case stopSync - case set(dataSource: RecordDataSource, baseURL: URL?) - - static func == (lhs: Action, rhs: Action) -> Bool { - switch (lhs, rhs) { - case (.sync, .sync), (.stopSync, .stopSync): - return true - case let (.set(rhsDataSource, rhsBaseURL), - .set(lhsDataSource, lhsBaseURL)): - return rhsDataSource.baseURL == lhsDataSource.baseURL && rhsBaseURL == lhsBaseURL - default: - return false - } - } - } - - private(set) var actionLog = [Action]() - - weak var delegate: SyncDelegate? - - func set(dataSource: RecordDataSource, baseURL: URL?) { - actionLog.append(.set(dataSource: dataSource, baseURL: baseURL)) - } - - func sync() { - actionLog.append(.sync) - delegate?.syncStarted() - DispatchQueue.main.async { - self.delegate?.syncStopped() - } - } - - func stopSync() { - actionLog.append(.stopSync) - delegate?.syncStopped() - } - - func cleanActionLog() { - actionLog.removeAll() - } -} diff --git a/MapboxVisionTests/Mocks/UIDeviceStubs.swift b/MapboxVisionTests/Mocks/UIDeviceStubs.swift index ed08db51..03a8eddd 100644 --- a/MapboxVisionTests/Mocks/UIDeviceStubs.swift +++ b/MapboxVisionTests/Mocks/UIDeviceStubs.swift @@ -127,12 +127,30 @@ final class UIDeviceIphoneXRStub: UIDevice { } } -final class UIDeviceIphoneNextGenerationStub: UIDevice { +final class UIDeviceIphone11Stub: UIDevice { override var modelID: String { return "iPhone12,1" } } +final class UIDeviceIphone11ProStub: UIDevice { + override var modelID: String { + return "iPhone12,3" + } +} + +final class UIDeviceIphone11ProMaxStub: UIDevice { + override var modelID: String { + return "iPhone12,5" + } +} + +final class UIDeviceIphoneNextGenerationStub: UIDevice { + override var modelID: String { + return "iPhone13,1" + } +} + final class UIDeviceIpadStub: UIDevice { override var modelID: String { return "iPad10,2" diff --git a/MapboxVisionTests/RecordCoordinatorTests.swift b/MapboxVisionTests/RecordCoordinatorTests.swift deleted file mode 100644 index 7c1e2b62..00000000 --- a/MapboxVisionTests/RecordCoordinatorTests.swift +++ /dev/null @@ -1,248 +0,0 @@ -import XCTest - -@testable import MapboxVision - -final class RecordingTestExpectation: XCTestExpectation { - var recordingPath: T! - - func fulfill(with path: T) { - recordingPath = path - fulfill() - } -} - -final class RecordCoordinatorTests: XCTestCase { - let videoSettings = VideoSettings(width: 960, height: 540, codec: .h264, fileType: .mp4, fileExtension: "mp4", bitRate: 6_000_000) - var coordinator: RecordCoordinator! - - let recordingStartedExpectation = RecordingTestExpectation(description: "Recording has been started") - let recordingStoppedExpectation = RecordingTestExpectation(description: "Recording has been stopped") - - override func setUp() { - super.setUp() - - let docLocations: [DocumentsLocation] = [.cache, .currentRecording, .recordings(.china), .recordings(.other)] - docLocations.map { $0.path }.forEach(removeDirectory) - - coordinator = RecordCoordinator() - coordinator.delegate = self - } - - override func tearDown() { - recordingStartedExpectation.recordingPath = nil - coordinator = nil - super.tearDown() - } - - func testStart() { - coordinator.startRecording(referenceTime: 0, videoSettings: videoSettings) { - XCTFail("Recording start has failed") - } - - wait(for: [recordingStartedExpectation], timeout: 1) - - XCTAssert(coordinator.isRecording, "Coordinator should be recording after recording start") - XCTAssert(directoryExists(at: DocumentsLocation.cache.path), "Cache should exist after recording is started") - - guard - let path = RecordingPath(existing: recordingStartedExpectation.recordingPath, settings: videoSettings), - directoryExists(at: path.recordingPath) - else { - XCTFail("Recording directory \(recordingStartedExpectation.recordingPath.debugDescription) should exist with created structure.") - return - } - - XCTAssert(directoryExists(at: path.imagesDirectoryPath), "\(path.imagesDirectoryPath) should exist") - } - - func testStop() { - coordinator.startRecording(referenceTime: 0, videoSettings: videoSettings) {} - - wait(for: [recordingStartedExpectation], timeout: 1) - - coordinator.stopRecording() - - wait(for: [recordingStoppedExpectation], timeout: 1) - - XCTAssert(!coordinator.isRecording, "Coordinator should not be recording after recording stop") - - let recordingPath = recordingStoppedExpectation.recordingPath.recordingPath - XCTAssert(directoryExists(at: recordingPath), "Recording should be saved at \(recordingPath) after recording is stopped") - } - - func testVideoClipping() { - // Given - // a list of clip requests - // When - // we try to make clips with them - // Then - // we should get the same number of video files on filesystem - performVideoClipping(with: [ - TestClipRequest(start: 1.0, end: 2.0), - TestClipRequest(start: 1.5, end: 2.5), - TestClipRequest(start: 0.0, end: 1.0), - TestClipRequest(start: 3.0, end: 4.0), - TestClipRequest(start: 3.0, end: 8.0) - ], "Failed to clip videos from a chunk") - } - - func testSessionRestarting() { - let expectation = XCTestExpectation(description: "All the recording sessions are finished") - let internalSessionDuration = 0.5 - let externalSessionDuration = 1.0 - - // Given - // SessionRecorder - let sessionRecorder = SessionRecorder(dependencies: SessionRecorder.Dependencies( - recorder: coordinator, - sessionManager: SessionManager(), - videoSettings: self.videoSettings, - getSeconds: { 0.0 }, - startSavingSession: { _ in }, - stopSavingSession: { } - )) - - var didCreateExternalFolder = false - let testFolderPath = DocumentsLocation.cache.path.appendingPathComponent("test", isDirectory: true) - - let recordDelegate = RecordDelegate() - recordDelegate.onRecordingStopped = { recordingPath in - if recordingPath.recordingPath.absolutePath == testFolderPath.absolutePath { - didCreateExternalFolder = true - expectation.fulfill() - } - } - sessionRecorder.delegate = recordDelegate - let videoSource = MockVideoSource() - videoSource.add(observer: sessionRecorder) - videoSource.start() - sessionRecorder.start() - - DispatchQueue.main.asyncAfter(deadline: .now() + internalSessionDuration) { - // When - // we manually restart session - sessionRecorder.stop() - sessionRecorder.start(mode: .external(path: testFolderPath)) - - DispatchQueue.main.asyncAfter(deadline: .now() + externalSessionDuration) { - sessionRecorder.stop() - } - } - - wait(for: [expectation], timeout: 2.0) - - // Then - // session files should be created for the session - XCTAssert(didCreateExternalFolder, "External session wasn't recorded") - } - - private func directoryExists(at path: String) -> Bool { - var isDirectory: ObjCBool = false - return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && isDirectory.boolValue - } - - private func removeDirectory(at path: String) { - do { - try FileManager.default.removeItem(atPath: path) - } catch CocoaError.fileNoSuchFile { - return - } catch { - assertionFailure("Directory removing has failed for path: \(path). Error: \(error)") - } - } - - private class TestClipRequest { - let start: Float - let end: Float - - init(start: Float, end: Float) { - self.start = start - self.end = end - } - } - - private class RecordDelegate: RecordCoordinatorDelegate { - var onRecordingStarted: ((String) -> Void)? - var onRecordingStopped: ((RecordingPath) -> Void)? - - func recordingStarted(path: String) { - onRecordingStarted?(path) - } - - func recordingStopped(recordingPath: RecordingPath) { - onRecordingStopped?(recordingPath) - } - } - - private func performVideoClipping(with requests: [TestClipRequest], _ message: String = "", line: UInt = #line) { - let expectation = XCTestExpectation(description: "Recording has been stopped") - let recordDurationBeforeClipRequests = 4.0 - let recordDurationAfterClipRequests = 1.0 - let videoSource = MockVideoSource() - videoSource.add(observer: coordinator) - videoSource.start() - - var didCreateAllClips = false - - let recordDelegate = RecordDelegate() - recordDelegate.onRecordingStopped = { path in - if let pathUrl = URL(string: path.recordingPath) { - let directoryContent = try? FileManager.default.contentsOfDirectory(at: pathUrl, includingPropertiesForKeys: nil, options: []) - - if let directoryContent = directoryContent { - let numberOfVideos = directoryContent.filter { - $0.pathExtension == path.settings.fileExtension - } - .count - - didCreateAllClips = numberOfVideos == requests.count - } - } - expectation.fulfill() - } - - coordinator.delegate = recordDelegate - coordinator.startRecording(referenceTime: 0, videoSettings: videoSettings) {} - DispatchQueue.main.asyncAfter(deadline: .now() + recordDurationBeforeClipRequests) { [unowned self] in - requests.forEach { - self.coordinator.makeClip(from: $0.start, to: $0.end) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + recordDurationAfterClipRequests) { [unowned self] in - self.coordinator.stopRecording() - } - } - - wait(for: [expectation], timeout: 10.0) - - XCTAssert(didCreateAllClips, message, line: line) - } -} - -extension RecordCoordinatorTests: RecordCoordinatorDelegate { - func recordingStarted(path: String) { - recordingStartedExpectation.fulfill(with: path) - } - - func recordingStopped(recordingPath: RecordingPath) { - recordingStoppedExpectation.fulfill(with: recordingPath) - } -} - -extension RecordCoordinator: VideoSourceObserver { - public func videoSource(_ videoSource: VideoSource, didOutput videoSample: VideoSample) { - handleFrame(videoSample.buffer) - } -} - -extension SessionRecorder: VideoSourceObserver { - public func videoSource(_ videoSource: VideoSource, didOutput videoSample: VideoSample) { - handleFrame(videoSample.buffer) - } -} - -extension String { - var absolutePath: String { - return nsString.resolvingSymlinksInPath - } -} diff --git a/MapboxVisionTests/RecordSynchronizerTests.swift b/MapboxVisionTests/RecordSynchronizerTests.swift deleted file mode 100644 index cfdcfb82..00000000 --- a/MapboxVisionTests/RecordSynchronizerTests.swift +++ /dev/null @@ -1,152 +0,0 @@ -import XCTest - -@testable import MapboxVision - -class RecordSynchronizerTests: XCTestCase { - typealias File = MockFileManager.File - - var networkClient: MockNetworkClient! - var dataSource: MockRecordDataSource! - var archiver: MockArchiver! - var recordSynchronizer: RecordSynchronizer! - var fileManager: MockFileManager! - var deviceInfo: DeviceInfoProvider! - var syncDelegate: ClosureSyncDelegate! - - private let data = [ - URL(fileURLWithPath: "/1", isDirectory: true): [ - File(url: URL(fileURLWithPath: "/1/gps.bin"), size: 20), - File(url: URL(fileURLWithPath: "/1/videos.json"), size: 3), - File(url: URL(fileURLWithPath: "/1/1.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/1/2.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/1/3.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/1/images/1.jpg"), size: 1), - File(url: URL(fileURLWithPath: "/1/images/2.jpg"), size: 1), - File(url: URL(fileURLWithPath: "/1/images/3.jpg"), size: 1), - ], - URL(fileURLWithPath: "/2", isDirectory: true): [ - File(url: URL(fileURLWithPath: "/2/gps.bin"), size: 15), - File(url: URL(fileURLWithPath: "/2/videos.json"), size: 1), - File(url: URL(fileURLWithPath: "/2/1.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/2/2.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/2/3.mp4"), size: 1), - File(url: URL(fileURLWithPath: "/2/images/1.jpg"), size: 1), - File(url: URL(fileURLWithPath: "/2/images/2.jpg"), size: 1), - File(url: URL(fileURLWithPath: "/2/images/3.jpg"), size: 1), - ], - URL(fileURLWithPath: "/3", isDirectory: true): [ - File(url: URL(fileURLWithPath: "/3/.synced"), size: 0), - ], - ] - - override func setUp() { - super.setUp() - - networkClient = MockNetworkClient() - dataSource = MockRecordDataSource() - archiver = MockArchiver() - fileManager = MockFileManager() - deviceInfo = DeviceInfoProvider() - syncDelegate = ClosureSyncDelegate() - recordSynchronizer = RecordSynchronizer(RecordSynchronizer.Dependencies( - networkClient: networkClient, - deviceInfo: deviceInfo, - archiver: archiver, - fileManager: fileManager - )) - recordSynchronizer.set(dataSource: dataSource, baseURL: nil) - recordSynchronizer.delegate = syncDelegate - } - - func testPositiveScenario() { - // Given - fileManager.data = data.values.flatMap { $0 } - dataSource.recordDirectories = data.keys.map { $0 } - - let expectation = XCTestExpectation(description: "Positive scenario") - - syncDelegate.onSyncStopped = { - self.positiveScenarioOnSyncStopped(expectation) - } - - // When - recordSynchronizer.sync() - - // Then - wait(for: [expectation], timeout: 10.0) - } - - func testStoppingSyncBeforeAllDataIsUploaded() { - // Given - fileManager.data = data.values.flatMap { $0 } - dataSource.recordDirectories = data.keys.map { $0 } - - let expectation = XCTestExpectation(description: "Cancellation") - - syncDelegate.onSyncStopped = { - let uploads: Set = [ - URL(fileURLWithPath: "/1/telemetry.zip"), - URL(fileURLWithPath: "/2/telemetry.zip"), - ] - - XCTAssert(Set(self.networkClient.uploaded.keys) == uploads, "Synchronizer should be able to upload only telemetry.") - expectation.fulfill() - } - - // When - recordSynchronizer.sync() - DispatchQueue.main.async { - self.recordSynchronizer.stopSync() - } - - // Then - wait(for: [expectation], timeout: 1) - } -} - -extension RecordSynchronizerTests { - func positiveScenarioOnSyncStopped(_ expectation: XCTestExpectation) { - XCTAssertFalse(fileManager.urls.contains(URL(fileURLWithPath: "/3", isDirectory: true)), "Empty dir should be removed") - - let archives = [ - URL(fileURLWithPath: "/1/telemetry.zip"), - URL(fileURLWithPath: "/2/telemetry.zip"), - URL(fileURLWithPath: "/1/images.zip"), - URL(fileURLWithPath: "/2/images.zip"), - ] - - XCTAssert(archiver.archives.count == archives.count, "Archiver should create \(archives.count) archives") - - archives.forEach { archive in - XCTAssertNotNil(archiver.archives[archive], "Archiver should create archive with right path") - let uploadDir = networkClient.uploaded[archive] - let dir = "\(archive.pathComponents[1])_en_US_\(deviceInfo.id)_\(deviceInfo.platformName)" - XCTAssert(uploadDir == dir, "\(archive) should be uploaded to \(dir). Actual upload dir: \(uploadDir ?? "none")") - XCTAssertFalse(fileManager.urls.contains(archive), "\(archive) should be removed after upload") - } - - XCTAssertFalse(fileManager.urls.contains(URL(fileURLWithPath: "/1/gps.bin")), "Bin file should be removed after archivation") - XCTAssertFalse(fileManager.urls.contains(URL(fileURLWithPath: "/1/videos.json")), "Json file should be removed after archivation") - - XCTAssert(fileManager.fileExists(atPath: "/1/.synced"), "Telemetry from first dir should be marked as synced") - XCTAssert(fileManager.fileExists(atPath: "/2/.synced"), "Telemetry from second dir should be marked as synced") - - let files = [ - URL(fileURLWithPath: "/1/1.mp4"), - URL(fileURLWithPath: "/1/2.mp4"), - URL(fileURLWithPath: "/1/3.mp4"), - URL(fileURLWithPath: "/2/1.mp4"), - URL(fileURLWithPath: "/2/2.mp4"), - URL(fileURLWithPath: "/2/3.mp4"), - ] - - files.forEach { file in - let uploadDir = networkClient.uploaded[file] - let dir = "\(file.pathComponents[1])_en_US_\(deviceInfo.id)_\(deviceInfo.platformName)" - XCTAssert(uploadDir == dir, "\(file) should be uploaded to \(dir). Actual upload dir: \(uploadDir ?? "none")") - XCTAssertFalse(fileManager.urls.contains(file), "\(file) should be removed after upload") - } - - expectation.fulfill() - } -} diff --git a/MapboxVisionTests/RecordingQuotaTests.swift b/MapboxVisionTests/RecordingQuotaTests.swift deleted file mode 100644 index 18c6d6f7..00000000 --- a/MapboxVisionTests/RecordingQuotaTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -@testable import MapboxVision -import XCTest - -class RecordingQuotaTests: XCTestCase { - // MARK: - Private properties - - private var refreshInterval: TimeInterval! - private var initialMemoryQuota: MemoryByte! - private var recordingQuota: RecordingQuota! - - // MARK: - Test functions - - override func setUp() { - super.setUp() - refreshInterval = 1.0 - initialMemoryQuota = 10 * .kByte - - recordingQuota = RecordingQuota(memoryQuota: initialMemoryQuota, refreshInterval: refreshInterval) - } - - override func tearDown() { - recordingQuota = nil - resetUserDefaultsState() - super.tearDown() - } - - func testReserveMethodDoesNotThrowIfTriesToReserveZeroBytes() { - // Given - let memoryZeroBytes: MemoryByte = 0 - - // When // Then - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryZeroBytes)) - } - - func testReserveMethodDoesThrowIfReservesMemoryLessThanQuota() { - // Given - let epsilon: MemoryByte = 1 - let memoryToReserveLessThanQuota = initialMemoryQuota - epsilon - - // When // Then - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveLessThanQuota)) - } - - func testReserveMethodAllowsToReserveMemorySeveralTimesWhileMemoryQuotaIsNotExceeded() { - // Given - let memoryToReserveAtOneTime = 1 * .kByte - let expectedNumberOfTimesWeCanReserveMemory = initialMemoryQuota / memoryToReserveAtOneTime - - // When - for _ in 1...expectedNumberOfTimesWeCanReserveMemory { - // Then - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveAtOneTime)) - } - } - - func testReserveMethodThrowsAfterSeveralReservationsButOnlyIfMemoryQuotaExceeded() { - // Given - let memoryToReserveAtOneTime = 1 * .kByte - let expectedNumberOfTimesWeCanReserveMemory = initialMemoryQuota / memoryToReserveAtOneTime - - for _ in 1...expectedNumberOfTimesWeCanReserveMemory { - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveAtOneTime)) - } - - // When // Then - XCTAssertThrowsError(try recordingQuota.reserve(memoryToReserve: memoryToReserveAtOneTime)) - } - - func testReserveMethodThrowsIfReservesMemoryMoreThanQuota() { - // Given - let epsilon: MemoryByte = 1 - let memoryToReserveExceedingQuota = initialMemoryQuota + epsilon - - // When // Then - XCTAssertThrowsError(try recordingQuota.reserve(memoryToReserve: memoryToReserveExceedingQuota)) - } - - func testReserveMethodDoesNotThrowIfCurrentQuotaWasReset() { - // Given - let epsilon: MemoryByte = 1 - let memoryToReserveMoreThanHalfAQuota = (initialMemoryQuota / 2) + epsilon - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveMoreThanHalfAQuota)) - - // When - Thread.sleep(forTimeInterval: refreshInterval) - - // Then - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveMoreThanHalfAQuota)) - } - - func testCurrentQuotaFullyResetsWhenRefreshIntervalIsTimedOut() { - // Given - let memoryToReserveEqualsToQuota = initialMemoryQuota! - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveEqualsToQuota)) - - // When - Thread.sleep(forTimeInterval: refreshInterval) - - // Then - XCTAssertNoThrow(try recordingQuota.reserve(memoryToReserve: memoryToReserveEqualsToQuota)) - } - - // MARK: - Private functions - - func resetUserDefaultsState() { - UserDefaults.standard.removeObject(forKey: RecordingQuota.Keys.lastResetTimeKey) - UserDefaults.standard.removeObject(forKey: RecordingQuota.Keys.recordingMemoryQuotaKey) - } -} diff --git a/MapboxVisionTests/VisionManagerTests.swift b/MapboxVisionTests/VisionManagerTests.swift index 990c3694..d97f76fa 100644 --- a/MapboxVisionTests/VisionManagerTests.swift +++ b/MapboxVisionTests/VisionManagerTests.swift @@ -4,12 +4,6 @@ import XCTest class VisionManagerTests: XCTestCase { var visionManager: VisionManager! var dependencies: VisionDependencies! - var recorder: MockSessionRecorder! - var synchronizer: MockSynchronizable! - var syncDelegate: ClosureSyncDelegate! - - let otherDataSource = SyncRecordDataSource(region: .other) - let chinaDataSource = SyncRecordDataSource(region: .china) override func setUp() { super.setUp() @@ -18,19 +12,12 @@ class VisionManagerTests: XCTestCase { let docLocations: [DocumentsLocation] = [.cache, .currentRecording, .recordings(.china), .recordings(.other)] docLocations.map { $0.path }.forEach(fileManager.removeDirectory) - recorder = MockSessionRecorder() - synchronizer = MockSynchronizable() dependencies = VisionDependencies( native: MockNative(), - synchronizer: synchronizer, - recorder: recorder, - dataProvider: MockDataProvider(), - deviceInfo: DeviceInfoProvider() + recorder: MockFrameRecorder(), + dataProvider: MockDataProvider() ) - syncDelegate = ClosureSyncDelegate() - synchronizer.delegate = syncDelegate - self.visionManager = VisionManager(dependencies: dependencies!, videoSource: MockVideoSource()) } @@ -74,272 +61,4 @@ class VisionManagerTests: XCTestCase { "VisionManager should be successfully deallocated after calling destroy and releasing the object" ) } - - // MARK: - Recording - - func testVisionManagerDoesNotRecordOnCountryChangeWhenNotStarted() { - // Given - // VisionManager with default country - - // When - visionManager.onCountryUpdated(.USA) - visionManager.onCountryUpdated(.UK) - visionManager.onCountryUpdated(.china) - visionManager.onCountryUpdated(.other) - visionManager.onCountryUpdated(.unknown) - - // Then - XCTAssert(recorder.actionsLog.isEmpty, "VisionManager should not record when not started.") - } - - func testVisionManagerRecordsDataInTheRightWayForCorrespondingCountries() { - // Given - // VisionManager with default country - - // When - // VisionManager is started - visionManager.start() - - // We set countries - visionManager.onCountryUpdated(.USA) - visionManager.onCountryUpdated(.UK) - visionManager.onCountryUpdated(.china) - visionManager.onCountryUpdated(.other) - visionManager.onCountryUpdated(.unknown) - - // Then - // VisionManager is expected to start internal recording on its start, not react to country update - // if synchronization region isn't changed, stop internal recording and start it again if the synchronization region is changed - let expectedActions: [MockSessionRecorder.Action] = [ - .startInternal, // initial - // nothing // USA - // nothing, // UK - .stop, // China - .startInternal, - .stop, // other - .startInternal, - .stop, // unknown - .startInternal, - ] - - XCTAssert( - recorder.actionsLog.elementsEqual(expectedActions), - """ - Internal recording doesn't react correctly to country change. - SessionRecorder's action log: \(recorder.actionsLog) doesn't match expected one: \(expectedActions) - """ - ) - } - - func testVisionManagerRecordsDataInTheRightWayForCorrespondingCountriesWithExternalRecordingEnabled() { - // Given - // VisionManager with default country - - // When - // VisionManager is started - visionManager.start() - - // External recording is started - try? visionManager.startRecording(to: "") - - // We set countries - visionManager.onCountryUpdated(.USA) - visionManager.onCountryUpdated(.UK) - visionManager.onCountryUpdated(.china) - visionManager.onCountryUpdated(.other) - visionManager.onCountryUpdated(.unknown) - - // Then - // VisionManager should not stop external recording if country is changed - let expectedActions: [MockSessionRecorder.Action] = [ - .startInternal, - .stop, - .startExternal(withPath: ""), - ] - - XCTAssert( - recorder.actionsLog.elementsEqual(expectedActions), - """ - External recording doesn't react correctly to country change. - SessionRecorder's action log: \(recorder.actionsLog) doesn't match expected one: \(expectedActions) - """ - ) - } - - func testVisionManagerStopsInternalRecordingOnStop() { - // Given - // VisionManager - - // When - visionManager.start() - visionManager.stop() - - // Then - let expectedActions: [MockSessionRecorder.Action] = [ - .startInternal, - .stop, - ] - - XCTAssert( - recorder.actionsLog.elementsEqual(expectedActions), - """ - Internal recording doesn't react correctly to VisionManager start and stop. - SessionRecorder's action log: \(recorder.actionsLog) doesn't match expected one: \(expectedActions) - """ - ) - } - - func testVisionManagerStopsExternalRecordingOnStop() { - // Given - // VisionManager - - // When - visionManager.start() - try? visionManager.startRecording(to: "") - visionManager.stop() - - // Then - let expectedActions: [MockSessionRecorder.Action] = [ - .startInternal, - .stop, - .startExternal(withPath: ""), - .stop, - ] - - XCTAssert( - recorder.actionsLog.elementsEqual(expectedActions), - """ - External recording doesn't react correctly to VisionManager start and stop. - SessionRecorder's action log: \(recorder.actionsLog) doesn't match expected one: \(expectedActions) - """ - ) - } - - func testVisionManagerStopsAllRecordingOnStop() { - // Given - // VisionManager - - // When - visionManager.start() - try? visionManager.startRecording(to: "") - visionManager.stopRecording() - visionManager.stop() - - // Then - let expectedActions: [MockSessionRecorder.Action] = [ - .startInternal, - .stop, - .startExternal(withPath: ""), - .stop, - .startInternal, - .stop - ] - - XCTAssert( - recorder.actionsLog.elementsEqual(expectedActions), - """ - Recording doesn't react correctly to VisionManager start, stop and single external recording request. - SessionRecorder's action log: \(recorder.actionsLog) doesn't match expected one: \(expectedActions) - """ - ) - } - - // MARK: - Syncing - - func testChangingCountries() { - // Given - let expectedActions: [(Country, [MockSynchronizable.Action])] = [ - (.unknown, [.stopSync]), - (.USA, [ - .stopSync, - .set(dataSource: otherDataSource, baseURL: URL(string: Constants.URL.defaultEventsEndpoint)!), - .sync - ]), - (.UK, []), - (.china, [ - .stopSync, - .set(dataSource: chinaDataSource, baseURL: URL(string: Constants.URL.chinaEventsEndpoint)!), - .sync - ]), - (.unknown, [.stopSync]), - ] - - // When - expectedActions.compactMap { $0.0 }.forEach(visionManager.onCountryUpdated) - - // Then - XCTAssertEqual(synchronizer.actionLog, Array(expectedActions.compactMap { $0.1 }.joined())) - } - - func testStopRecordingNotTriggerSyncForDefaultCountry() { - // Given - // VisionManager with default .unknown country - - // When - visionManager.start() - visionManager.stop() - - // Then - XCTAssert(synchronizer.actionLog.isEmpty, "Stopped recording shouldn't initiate sync") - } - - func testStopRecordingTriggersSyncForOtherCountry() { - // Given - visionManager.onCountryUpdated(.other) - synchronizer.cleanActionLog() - - let stopExpectation = XCTestExpectation(description: "Sync stop expectation") - syncDelegate.onSyncStopped = { - stopExpectation.fulfill() - } - - // When - visionManager.start() - visionManager.stop() - - // Then - wait(for: [stopExpectation], timeout: 1) - - XCTAssertEqual(synchronizer.actionLog, [.sync], "Stopped recording for other country should initiate sync") - } - - func testStopRecordingTriggersSyncForChinaCountry() { - // Given - visionManager.onCountryUpdated(.china) - synchronizer.cleanActionLog() - - let stopExpectation = XCTestExpectation(description: "Sync stop expectation") - syncDelegate.onSyncStopped = { - stopExpectation.fulfill() - } - - // When - visionManager.start() - visionManager.stop() - - // Then - wait(for: [stopExpectation], timeout: 1) - - XCTAssertEqual(synchronizer.actionLog, [.sync], "Stopped recording for china country should initiate sync") - } - - func testCorrectCountry() { - // Given - visionManager.onCountryUpdated(.china) - - let startExpectation = XCTestExpectation(description: "Sync stop expectation") - syncDelegate.onSyncStarted = { - startExpectation.fulfill() - } - - // When - visionManager.start() - visionManager.onCountryUpdated(.other) - - wait(for: [startExpectation], timeout: 1) - - // Then - XCTAssertFalse(chinaDataSource.recordDirectories.isEmpty) - XCTAssertTrue(otherDataSource.recordDirectories.isEmpty) - } } From 3f51f1f4a9d0be36151b0eee76fc834eddb2e6f7 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Sat, 16 Nov 2019 09:02:57 +0300 Subject: [PATCH 16/39] Clean old telemetry --- .../VisionManager/VisionManager.swift | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/MapboxVision/VisionManager/VisionManager.swift b/MapboxVision/VisionManager/VisionManager.swift index 719c3234..878aa331 100644 --- a/MapboxVision/VisionManager/VisionManager.swift +++ b/MapboxVision/VisionManager/VisionManager.swift @@ -141,6 +141,20 @@ public final class VisionManager: BaseVisionManager { state = .uninitialized } + // MARK: - Internal + + override func prepareForBackground() { + guard state.isStarted else { return } + isStoppedForBackground = true + pause() + } + + override func prepareForForeground() { + guard isStoppedForBackground else { return } + isStoppedForBackground = false + resume() + } + // MARK: - Private private enum State { @@ -187,6 +201,8 @@ public final class VisionManager: BaseVisionManager { state = .initialized(videoSource: videoSource) dependencies.native.videoSource = VideoSourceObserverProxy(withVideoSource: videoSource) + + cleanupTelemetry() } deinit { @@ -215,16 +231,13 @@ public final class VisionManager: BaseVisionManager { dependencies.native.stop() } - override func prepareForBackground() { - guard state.isStarted else { return } - isStoppedForBackground = true - pause() - } - - override func prepareForForeground() { - guard isStoppedForBackground else { return } - isStoppedForBackground = false - resume() + // Removes old recordings and telemetry since new paths are used in core. + // Added in 0.11.0. Remove after reaching major adoption of 0.11.0 or later versions. + private func cleanupTelemetry() { + let locations: [DocumentsLocation] = [.cache, .currentRecording, .recordings(.china), .recordings(.other)] + locations.forEach { location in + try? FileManager.default.removeItem(atPath: location.path) + } } } From 4bf70ad68ae8bc970a66924773c93d4222b1a2c8 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Sat, 16 Nov 2019 12:50:34 +0300 Subject: [PATCH 17/39] Review fixes --- .../Services/Recording/VideoRecorder.swift | 11 ++++----- .../Services/Recording/VideoTrimmer.swift | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index 8af23677..67064b1f 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -56,20 +56,19 @@ final class VideoRecorder { } func stopRecording(completion: (() -> Void)?) { - writerQueue.sync { [weak self] in + writerQueue.sync { let cleanup = { - self?.isRecording = false - self?.currentAssetWriter = nil - self?.startTime = nil + self.isRecording = false + self.currentAssetWriter = nil + self.startTime = nil completion?() } - guard let writer = self?.currentAssetWriter, writer.status == .writing else { + guard let writer = self.currentAssetWriter, writer.status == .writing else { cleanup() return } guard - let self = self, let assetWriterInput = self.currentAssetWriterInput else { return } diff --git a/MapboxVision/Services/Recording/VideoTrimmer.swift b/MapboxVision/Services/Recording/VideoTrimmer.swift index 669cc5b8..997d83d0 100644 --- a/MapboxVision/Services/Recording/VideoTrimmer.swift +++ b/MapboxVision/Services/Recording/VideoTrimmer.swift @@ -5,7 +5,8 @@ private let timeScale: CMTimeScale = 600 private let fileType: AVFileType = .mp4 enum VideoTrimmerError: LocalizedError { - case sourceNotExportable + case notSuitableSource + case incorrectConfiguration } final class VideoTrimmer { @@ -18,8 +19,12 @@ final class VideoTrimmer { ] let asset = AVURLAsset(url: sourceURL, options: options) - guard asset.isExportable else { - completion(VideoTrimmerError.sourceNotExportable) + guard + asset.isExportable, + let videoAssetTrack = asset.tracks(withMediaType: .video).first + else { + assertionFailure("Source asset is not exportable or doesn't contain a video track. Asset: \(asset).") + completion(VideoTrimmerError.notSuitableSource) return } @@ -28,11 +33,7 @@ final class VideoTrimmer { let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID()) else { assertionFailure("Unable to add video track to composition \(composition).") - return - } - - guard let videoAssetTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first else { - assertionFailure("Unable to obtain video track from asset \(asset).") + completion(VideoTrimmerError.incorrectConfiguration) return } @@ -46,11 +47,16 @@ final class VideoTrimmer { try videoTrack.insertTimeRange(timeRangeForCurrentSlice, of: videoAssetTrack, at: CMTime()) } catch { completion(error) + return } guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough) - else { return } + else { + assertionFailure("Unable to create an export session with composition \(composition).") + completion(VideoTrimmerError.incorrectConfiguration) + return + } exportSession.outputURL = URL(fileURLWithPath: clip.path) exportSession.outputFileType = fileType From aa2cc98ea68f54f5caa4a010d821ed356949bc28 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Sat, 16 Nov 2019 17:01:51 +0300 Subject: [PATCH 18/39] Split platform interface --- MapboxVision.xcodeproj/project.pbxproj | 16 +++- MapboxVision/Core/FileSystem.swift | 26 ++++++ MapboxVision/Core/Media.swift | 40 +++++++++ MapboxVision/Core/Platform.swift | 85 ------------------- MapboxVision/Core/Telemetry.swift | 30 +++++++ MapboxVision/Services/EventsManager.swift | 8 +- .../Services/Network/NetworkClient.swift | 1 + .../Services/Recording/VideoRecorder.swift | 6 +- .../VisionManager/ManagerDependencies.swift | 25 ++---- 9 files changed, 123 insertions(+), 114 deletions(-) create mode 100644 MapboxVision/Core/FileSystem.swift create mode 100644 MapboxVision/Core/Media.swift delete mode 100644 MapboxVision/Core/Platform.swift create mode 100644 MapboxVision/Core/Telemetry.swift diff --git a/MapboxVision.xcodeproj/project.pbxproj b/MapboxVision.xcodeproj/project.pbxproj index 1bcda533..85d8dc80 100644 --- a/MapboxVision.xcodeproj/project.pbxproj +++ b/MapboxVision.xcodeproj/project.pbxproj @@ -28,7 +28,7 @@ 2E4EE24D20F05045003431B3 /* VisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23E20EFD651003431B3 /* VisionManager.swift */; }; 2E4EE24E20F05045003431B3 /* VisionPresentationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23F20EFD651003431B3 /* VisionPresentationViewController.swift */; }; 2E4EE25520F05371003431B3 /* VideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE25420F05370003431B3 /* VideoSettings.swift */; }; - 2E4EE27320F05652003431B3 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23820EFD5BB003431B3 /* Platform.swift */; }; + 2E4EE27320F05652003431B3 /* FileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23820EFD5BB003431B3 /* FileSystem.swift */; }; 2E4EE27420F05652003431B3 /* CoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE23920EFD5BB003431B3 /* CoreConfig.swift */; }; 2E4EE27720F05659003431B3 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22020EFD5BA003431B3 /* NetworkClient.swift */; }; 2E4EE27820F05659003431B3 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4EE22120EFD5BA003431B3 /* Path.swift */; }; @@ -45,6 +45,8 @@ 2E4EE2E520F0CC63003431B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2E4EE2E220F0CC63003431B3 /* Assets.xcassets */; }; 2E50E4AE22FC7B2C00A2C03D /* SyncRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E50E4AD22FC7B2C00A2C03D /* SyncRegion.swift */; }; 2E6033542142900C0050EDB3 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E6033532142900C0050EDB3 /* Images.swift */; }; + 2E68DBED23800C7600580BE4 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E68DBEC23800C7600580BE4 /* Telemetry.swift */; }; + 2E68DBEF23800C9B00580BE4 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E68DBEE23800C9B00580BE4 /* Media.swift */; }; 2E7F14FB228E2D1400BAF585 /* BaseVisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FA228E2D1400BAF585 /* BaseVisionManager.swift */; }; 2E7F14FD228E92D900BAF585 /* VisionReplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FC228E92D900BAF585 /* VisionReplayManager.swift */; }; 2E7F14FF228E982D00BAF585 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7F14FE228E982D00BAF585 /* VideoPlayer.swift */; }; @@ -115,7 +117,7 @@ 2E4EE22B20EFD5BA003431B3 /* RecordingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingPath.swift; sourceTree = ""; }; 2E4EE23020EFD5BA003431B3 /* ManagerDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagerDependencies.swift; sourceTree = ""; }; 2E4EE23420EFD5BA003431B3 /* DataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvider.swift; sourceTree = ""; }; - 2E4EE23820EFD5BB003431B3 /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; + 2E4EE23820EFD5BB003431B3 /* FileSystem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystem.swift; sourceTree = ""; }; 2E4EE23920EFD5BB003431B3 /* CoreConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreConfig.swift; sourceTree = ""; }; 2E4EE23E20EFD651003431B3 /* VisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionManager.swift; sourceTree = ""; }; 2E4EE23F20EFD651003431B3 /* VisionPresentationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionPresentationViewController.swift; sourceTree = ""; }; @@ -129,6 +131,8 @@ 2E4EE2E220F0CC63003431B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2E50E4AD22FC7B2C00A2C03D /* SyncRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncRegion.swift; sourceTree = ""; }; 2E6033532142900C0050EDB3 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; + 2E68DBEC23800C7600580BE4 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; + 2E68DBEE23800C9B00580BE4 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; 2E7F14FA228E2D1400BAF585 /* BaseVisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVisionManager.swift; sourceTree = ""; }; 2E7F14FC228E92D900BAF585 /* VisionReplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisionReplayManager.swift; sourceTree = ""; }; 2E7F14FE228E982D00BAF585 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; @@ -340,7 +344,9 @@ isa = PBXGroup; children = ( 2E4EE23920EFD5BB003431B3 /* CoreConfig.swift */, - 2E4EE23820EFD5BB003431B3 /* Platform.swift */, + 2E4EE23820EFD5BB003431B3 /* FileSystem.swift */, + 2E68DBEC23800C7600580BE4 /* Telemetry.swift */, + 2E68DBEE23800C9B00580BE4 /* Media.swift */, ); path = Core; sourceTree = ""; @@ -721,7 +727,7 @@ 2E7F14FF228E982D00BAF585 /* VideoPlayer.swift in Sources */, 2E50E4AE22FC7B2C00A2C03D /* SyncRegion.swift in Sources */, 2E4EE2BA20F05ACB003431B3 /* Result.swift in Sources */, - 2E4EE27320F05652003431B3 /* Platform.swift in Sources */, + 2E4EE27320F05652003431B3 /* FileSystem.swift in Sources */, 2E19772B211DDD5900311B95 /* DeviceChecker.swift in Sources */, 2E4EE27820F05659003431B3 /* Path.swift in Sources */, 2E4EE25520F05371003431B3 /* VideoSettings.swift in Sources */, @@ -731,11 +737,13 @@ 2E4EE27720F05659003431B3 /* NetworkClient.swift in Sources */, 2E4EE2AC20F0590F003431B3 /* LocationManager.swift in Sources */, 2E4EE2A220F058B9003431B3 /* AVCaptureConnection+Orientation.swift in Sources */, + 2E68DBED23800C7600580BE4 /* Telemetry.swift in Sources */, 2E4EE24D20F05045003431B3 /* VisionManager.swift in Sources */, 2E4EE28020F05661003431B3 /* RecordingPath.swift in Sources */, 2E4EE28820F05665003431B3 /* DataProvider.swift in Sources */, 2E4EE2CC20F06A11003431B3 /* CGPoint+Convert.swift in Sources */, 2EDD8707219ED89E00760C6D /* Constants.swift in Sources */, + 2E68DBEF23800C9B00580BE4 /* Media.swift in Sources */, 2E88A9BA213ED5DC008A27C8 /* ModelPerformance.swift in Sources */, 2E4EE24E20F05045003431B3 /* VisionPresentationViewController.swift in Sources */, 2EFF563F210B4A5B00B8D512 /* VideoTrimmer.swift in Sources */, diff --git a/MapboxVision/Core/FileSystem.swift b/MapboxVision/Core/FileSystem.swift new file mode 100644 index 00000000..63721cb6 --- /dev/null +++ b/MapboxVision/Core/FileSystem.swift @@ -0,0 +1,26 @@ +import Foundation + +final class FileSystem: NSObject { + private let archiver: Archiver + + init(archiver: Archiver) { + self.archiver = archiver + } +} + +extension FileSystem: FileSystemInterface { + func archiveFiles(filePaths: [String], archivePath: String, callback: @escaping SuccessCallback) { + DispatchQueue.global(qos: .utility).async { + do { + try self.archiver.archive(filePaths.map(URL.init(fileURLWithPath:)), + destination: URL(fileURLWithPath: archivePath)) + } catch { + assertionFailure("ERROR: archiving failed with error: \(error.localizedDescription)") + callback(false) + return + } + + callback(true) + } + } +} diff --git a/MapboxVision/Core/Media.swift b/MapboxVision/Core/Media.swift new file mode 100644 index 00000000..e6ec2733 --- /dev/null +++ b/MapboxVision/Core/Media.swift @@ -0,0 +1,40 @@ +import Foundation + +final class Media: NSObject { + private let recorder: FrameRecordable + private let videoTrimmer: VideoTrimmer + + init(recorder: FrameRecordable, videoTrimmer: VideoTrimmer) { + self.recorder = recorder + self.videoTrimmer = videoTrimmer + } +} + +extension Media: MediaInterface { + func startVideoRecording(filePath: String) { + recorder.startRecording(to: filePath, settings: .lowQuality) + } + + func stopVideoRecording() { + recorder.stopRecording() + } + + func makeVideoClips(inputFilePath: String, clips: [VideoClip], callback: @escaping SuccessCallback) { + var success = true + let group = DispatchGroup() + + for clip in clips { + group.enter() + videoTrimmer.trimVideo(source: inputFilePath, clip: clip) { error in + if error != nil { + success = false + } + group.leave() + } + } + + group.notify(queue: DispatchQueue.global(qos: .utility)) { + callback(success) + } + } +} diff --git a/MapboxVision/Core/Platform.swift b/MapboxVision/Core/Platform.swift deleted file mode 100644 index 0a9b29bc..00000000 --- a/MapboxVision/Core/Platform.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation -import MapboxVisionNative - -typealias TelemetryFileMetadata = [String: String] - -final class Platform: NSObject { - struct Dependencies { - let recorder: FrameRecordable? - let videoTrimmer: VideoTrimmer? - let eventsManager: EventsManager - let archiver: Archiver? - } - - private let dependencies: Dependencies - - init(dependencies: Dependencies) { - self.dependencies = dependencies - } -} - -extension Platform: PlatformInterface { - func setSyncUrl(_ url: String) { - dependencies.eventsManager.set(baseURL: URL(string: url)) - } - - func sendTelemetry(name: String, entries: [TelemetryEntry]) { - let entries = Dictionary(entries.map { ($0.key, $0.value) }) { first, _ in - assertionFailure("Duplicated key in telemetry entries.") - return first - } - - dependencies.eventsManager.sendEvent(name: name, entries: entries) - } - - func sendTelemetryFile(path: String, metadata: TelemetryFileMetadata, callback: @escaping SuccessCallback) { - dependencies.eventsManager.upload(file: path, metadata: metadata) { error in callback(error == nil) } - } - - func startVideoRecording(filePath: String) { - dependencies.recorder?.startRecording(to: filePath, settings: .lowQuality) - } - - func stopVideoRecording() { - dependencies.recorder?.stopRecording(completion: nil) - } - - func makeVideoClips(inputFilePath: String, clips: [VideoClip], callback: @escaping SuccessCallback) { - guard let videoTrimmer = dependencies.videoTrimmer else { - callback(false) - return - } - - var success = true - let group = DispatchGroup() - - for clip in clips { - group.enter() - videoTrimmer.trimVideo(source: inputFilePath, clip: clip) { error in - if error != nil { - success = false - } - group.leave() - } - } - - group.notify(queue: DispatchQueue.global(qos: .utility)) { - callback(success) - } - } - - func archiveFiles(filePaths: [String], archivePath: String, callback: @escaping SuccessCallback) { - DispatchQueue.global(qos: .utility).async { - do { - try self.dependencies.archiver?.archive(filePaths.map(URL.init(fileURLWithPath:)), - destination: URL(fileURLWithPath: archivePath)) - } catch { - assertionFailure("ERROR: archiving failed with error: \(error.localizedDescription)") - callback(false) - return - } - - callback(true) - } - } -} diff --git a/MapboxVision/Core/Telemetry.swift b/MapboxVision/Core/Telemetry.swift new file mode 100644 index 00000000..132049b4 --- /dev/null +++ b/MapboxVision/Core/Telemetry.swift @@ -0,0 +1,30 @@ +import Foundation + +typealias TelemetryFileMetadata = [String: String] + +final class Telemetry: NSObject { + private let networkClient: NetworkClient + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } +} + +extension Telemetry: TelemetryInterface { + func setSyncUrl(_ url: String) { + networkClient.set(baseURL: URL(string: url)) + } + + func sendTelemetry(name: String, entries: [TelemetryEntry]) { + let entries = Dictionary(entries.map { ($0.key, $0.value) }) { first, _ in + assertionFailure("Duplicated key in telemetry entries.") + return first + } + + networkClient.sendEvent(name: name, entries: entries) + } + + func sendTelemetryFile(path: String, metadata: TelemetryFileMetadata, callback: @escaping SuccessCallback) { + networkClient.upload(file: path, metadata: metadata) { error in callback(error == nil) } + } +} diff --git a/MapboxVision/Services/EventsManager.swift b/MapboxVision/Services/EventsManager.swift index 93a45f42..1497098b 100644 --- a/MapboxVision/Services/EventsManager.swift +++ b/MapboxVision/Services/EventsManager.swift @@ -29,10 +29,6 @@ final class EventsManager { manager.sendTurnstileEvent() manager.isMetricsEnabled = true } - - func sendEvent(name: String, entries: [String: Any]) { - manager.enqueueEvent(withName: name, attributes: entries) - } } extension EventsManager: NetworkClient { @@ -40,6 +36,10 @@ extension EventsManager: NetworkClient { manager.baseURL = baseURL } + func sendEvent(name: String, entries: [String: Any]) { + manager.enqueueEvent(withName: name, attributes: entries) + } + func upload(file: String, metadata: TelemetryFileMetadata, completion: @escaping (Error?) -> Void) { manager.postMetadata([metadata], filePaths: [file], completionHandler: completion) } diff --git a/MapboxVision/Services/Network/NetworkClient.swift b/MapboxVision/Services/Network/NetworkClient.swift index 7fa69584..79f3aee8 100644 --- a/MapboxVision/Services/Network/NetworkClient.swift +++ b/MapboxVision/Services/Network/NetworkClient.swift @@ -3,6 +3,7 @@ import MapboxVisionNative protocol NetworkClient { func set(baseURL: URL?) + func sendEvent(name: String, entries: [String: Any]) func upload(file: String, metadata: TelemetryFileMetadata, completion: @escaping (Error?) -> Void) func cancel() } diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index 67064b1f..19bc64f2 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -20,7 +20,7 @@ private extension VideoSettings { protocol FrameRecordable { func startRecording(to path: String, settings: VideoSettings) - func stopRecording(completion: (() -> Void)?) + func stopRecording() func handle(frame: CMSampleBuffer) } @@ -55,14 +55,14 @@ final class VideoRecorder { } } - func stopRecording(completion: (() -> Void)?) { + func stopRecording() { writerQueue.sync { let cleanup = { self.isRecording = false self.currentAssetWriter = nil self.startTime = nil - completion?() } + guard let writer = self.currentAssetWriter, writer.status == .writing else { cleanup() return diff --git a/MapboxVision/VisionManager/ManagerDependencies.swift b/MapboxVision/VisionManager/ManagerDependencies.swift index b9835e0a..c9fe233e 100644 --- a/MapboxVision/VisionManager/ManagerDependencies.swift +++ b/MapboxVision/VisionManager/ManagerDependencies.swift @@ -15,14 +15,13 @@ struct VisionDependencies { static func `default`() -> VisionDependencies { let recorder = VideoRecorder() - let platform = Platform(dependencies: Platform.Dependencies( - recorder: recorder, - videoTrimmer: VideoTrimmer(), - eventsManager: EventsManager(), - archiver: RecordArchiver() - )) + let platform = Platform( + telemetry: Telemetry(networkClient: EventsManager()), + fileSystem: FileSystem(archiver: RecordArchiver()), + media: Media(recorder: recorder, videoTrimmer: VideoTrimmer()) + ) - let native = VisionManagerNative.create(withPlatform: platform) + let native = VisionManagerNative.create(with: platform) let dataProvider = RealtimeDataProvider(dependencies: RealtimeDataProvider.Dependencies( native: native, @@ -45,17 +44,7 @@ struct ReplayDependencies { throw CocoaError(.fileNoSuchFile) } let player = try VideoPlayer(path: videoPath) - - let eventsManager = EventsManager() - - let platform = Platform(dependencies: Platform.Dependencies( - recorder: nil, - videoTrimmer: nil, - eventsManager: eventsManager, - archiver: RecordArchiver() - )) - - let native = VisionReplayManagerNative.create(withPlatform: platform, recordPath: recordPath) + let native = VisionReplayManagerNative.create(withRecordPath: recordPath) return ReplayDependencies(native: native, player: player) } From 72a052bbac397d6b5ea8382dd5ca2833fc428953 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Sat, 16 Nov 2019 18:25:39 +0300 Subject: [PATCH 19/39] Review fixes --- MapboxVision/Core/Media.swift | 4 ++-- .../Services/Recording/VideoRecorder.swift | 4 ++-- MapboxVision/Services/Recording/VideoTrimmer.swift | 14 ++++++++------ .../VisionManager/ManagerDependencies.swift | 2 +- MapboxVisionTests/Mocks/MockFrameRecorder.swift | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/MapboxVision/Core/Media.swift b/MapboxVision/Core/Media.swift index e6ec2733..0252f64c 100644 --- a/MapboxVision/Core/Media.swift +++ b/MapboxVision/Core/Media.swift @@ -1,10 +1,10 @@ import Foundation final class Media: NSObject { - private let recorder: FrameRecordable + private let recorder: FrameRecorder private let videoTrimmer: VideoTrimmer - init(recorder: FrameRecordable, videoTrimmer: VideoTrimmer) { + init(recorder: FrameRecorder, videoTrimmer: VideoTrimmer) { self.recorder = recorder self.videoTrimmer = videoTrimmer } diff --git a/MapboxVision/Services/Recording/VideoRecorder.swift b/MapboxVision/Services/Recording/VideoRecorder.swift index 19bc64f2..603934b1 100644 --- a/MapboxVision/Services/Recording/VideoRecorder.swift +++ b/MapboxVision/Services/Recording/VideoRecorder.swift @@ -18,7 +18,7 @@ private extension VideoSettings { } } -protocol FrameRecordable { +protocol FrameRecorder { func startRecording(to path: String, settings: VideoSettings) func stopRecording() func handle(frame: CMSampleBuffer) @@ -137,7 +137,7 @@ final class VideoRecorder { } } -extension VideoRecorder: FrameRecordable { +extension VideoRecorder: FrameRecorder { func handle(frame: CMSampleBuffer) { handleFrame(frame) { _ in } } diff --git a/MapboxVision/Services/Recording/VideoTrimmer.swift b/MapboxVision/Services/Recording/VideoTrimmer.swift index 997d83d0..6559aba8 100644 --- a/MapboxVision/Services/Recording/VideoTrimmer.swift +++ b/MapboxVision/Services/Recording/VideoTrimmer.swift @@ -1,9 +1,6 @@ import AVFoundation import Foundation -private let timeScale: CMTimeScale = 600 -private let fileType: AVFileType = .mp4 - enum VideoTrimmerError: LocalizedError { case notSuitableSource case incorrectConfiguration @@ -12,6 +9,11 @@ enum VideoTrimmerError: LocalizedError { final class VideoTrimmer { typealias TrimCompletion = (Error?) -> Void + private enum Constants { + static let timeScale: CMTimeScale = 600 + static let fileType: AVFileType = .mp4 + } + func trimVideo(source: String, clip: VideoClip, completion: @escaping TrimCompletion) { let sourceURL = URL(fileURLWithPath: source) let options = [ @@ -37,8 +39,8 @@ final class VideoTrimmer { return } - let startTime = CMTime(seconds: Double(clip.startTime), preferredTimescale: timeScale) - let endTime = CMTime(seconds: Double(clip.stopTime), preferredTimescale: timeScale) + let startTime = CMTime(seconds: Double(clip.startTime), preferredTimescale: Constants.timeScale) + let endTime = CMTime(seconds: Double(clip.stopTime), preferredTimescale: Constants.timeScale) let durationOfCurrentSlice = CMTimeSubtract(endTime, startTime) let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTime, duration: durationOfCurrentSlice) @@ -59,7 +61,7 @@ final class VideoTrimmer { } exportSession.outputURL = URL(fileURLWithPath: clip.path) - exportSession.outputFileType = fileType + exportSession.outputFileType = Constants.fileType exportSession.shouldOptimizeForNetworkUse = true exportSession.exportAsynchronously { diff --git a/MapboxVision/VisionManager/ManagerDependencies.swift b/MapboxVision/VisionManager/ManagerDependencies.swift index c9fe233e..7ce46c37 100644 --- a/MapboxVision/VisionManager/ManagerDependencies.swift +++ b/MapboxVision/VisionManager/ManagerDependencies.swift @@ -9,7 +9,7 @@ struct BaseDependencies { struct VisionDependencies { let native: VisionManagerNativeProtocol - let recorder: FrameRecordable + let recorder: FrameRecorder let dataProvider: DataProvider static func `default`() -> VisionDependencies { diff --git a/MapboxVisionTests/Mocks/MockFrameRecorder.swift b/MapboxVisionTests/Mocks/MockFrameRecorder.swift index 798b336a..92685d28 100644 --- a/MapboxVisionTests/Mocks/MockFrameRecorder.swift +++ b/MapboxVisionTests/Mocks/MockFrameRecorder.swift @@ -1,7 +1,7 @@ import Foundation @testable import MapboxVision -class MockFrameRecorder: NSObject, FrameRecordable { +class MockFrameRecorder: NSObject, FrameRecorder { enum Action: Equatable { case startRecording case stopRecording From 4312493c312b52b8636fa3e8636437b5af427551 Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Tue, 19 Nov 2019 16:44:12 +0300 Subject: [PATCH 20/39] Remove location update in background --- CHANGELOG.md | 1 + MapboxVision/Services/LocationManager.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8d80e8..6e1c5f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.11.0 - Unreleased ### Vision +- Removed location update in background ### AR diff --git a/MapboxVision/Services/LocationManager.swift b/MapboxVision/Services/LocationManager.swift index ec5519bc..22013150 100644 --- a/MapboxVision/Services/LocationManager.swift +++ b/MapboxVision/Services/LocationManager.swift @@ -17,7 +17,7 @@ final class LocationManager: NSObject, CLLocationManagerDelegate { switch CLLocationManager.authorizationStatus() { case .notDetermined: - locationManager.requestAlwaysAuthorization() + locationManager.requestWhenInUseAuthorization() case .restricted, .denied: isReady = false case .authorizedAlways, .authorizedWhenInUse: From 87371ab4d251bc39838e121ff6c25631e2722683 Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 11 Dec 2019 00:51:01 +0300 Subject: [PATCH 21/39] Stop sending location updates after manager is stopped --- MapboxVision/Services/LocationManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MapboxVision/Services/LocationManager.swift b/MapboxVision/Services/LocationManager.swift index 22013150..da2b21c5 100644 --- a/MapboxVision/Services/LocationManager.swift +++ b/MapboxVision/Services/LocationManager.swift @@ -41,11 +41,12 @@ final class LocationManager: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let handler = locationHandler else { return } + guard let handler = locationHandler, isStarted else { return } locations.forEach(handler) } func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + guard isStarted else { return } headingHandler?(newHeading) } From 8081aac1aa909bc68ecd39de8b7c1450da449bdc Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 11 Dec 2019 11:44:41 +0300 Subject: [PATCH 22/39] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1c5f39..a2542d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Vision - Removed location update in background +- Fixed a crash on receiving location updates while stopping `VisionManager` ### AR From e0dd176d3dece315cf69f70b5beabed551e0971b Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 11 Dec 2019 12:43:14 +0300 Subject: [PATCH 23/39] Set and reset handlers on start / stop of RealtimeDataProvider --- MapboxVision/Services/DataProvider.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MapboxVision/Services/DataProvider.swift b/MapboxVision/Services/DataProvider.swift index 7522d3d6..6d044fe2 100644 --- a/MapboxVision/Services/DataProvider.swift +++ b/MapboxVision/Services/DataProvider.swift @@ -18,13 +18,14 @@ final class RealtimeDataProvider: DataProvider { init(dependencies: Dependencies) { self.dependencies = dependencies - dependencies.motionManager.handler = dependencies.native.sensors.setDeviceMotion - dependencies.locationManager.locationHandler = dependencies.native.sensors.setGPS - dependencies.locationManager.headingHandler = dependencies.native.sensors.setHeading } func start() { + dependencies.locationManager.locationHandler = dependencies.native.sensors.setGPS + dependencies.locationManager.headingHandler = dependencies.native.sensors.setHeading dependencies.locationManager.start() + + dependencies.motionManager.handler = dependencies.native.sensors.setDeviceMotion dependencies.motionManager.start(updateInterval: Constants.motionUpdateInterval) } @@ -32,6 +33,10 @@ final class RealtimeDataProvider: DataProvider { func stop() { dependencies.locationManager.stop() + dependencies.locationManager.locationHandler = nil + dependencies.locationManager.headingHandler = nil + dependencies.motionManager.stop() + dependencies.motionManager.handler = nil } } From f68261b3b77e371ceec10b3a85c63c0963ee14a4 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Thu, 12 Dec 2019 14:50:25 +0300 Subject: [PATCH 24/39] Add initial ObservableVideoSourceTests class. --- MapboxVision.xcodeproj/project.pbxproj | 4 +++ .../ObservableVideoSourceTests.swift | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 MapboxVisionTests/ObservableVideoSourceTests.swift diff --git a/MapboxVision.xcodeproj/project.pbxproj b/MapboxVision.xcodeproj/project.pbxproj index 85d8dc80..c9d4ce33 100644 --- a/MapboxVision.xcodeproj/project.pbxproj +++ b/MapboxVision.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 2EDFFD73224BBD3E003BB718 /* ObservableVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDFFD72224BBD3E003BB718 /* ObservableVideoSource.swift */; }; 2EFF563F210B4A5B00B8D512 /* VideoTrimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EFF563E210B4A5B00B8D512 /* VideoTrimmer.swift */; }; 3A11FEDF2282BCD300B68E5B /* DeviceCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */; }; + 3A2B80BA239E4AE800166F2E /* ObservableVideoSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2B80B9239E4AE800166F2E /* ObservableVideoSourceTests.swift */; }; 3A7676AE22D647FE00ABC483 /* DeviceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E19772A211DDD5900311B95 /* DeviceChecker.swift */; }; 3AAB2EFB22858F28000D052E /* UIDeviceStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB2EFA22858F28000D052E /* UIDeviceStubs.swift */; }; 5A006F4A231546250097FADF /* VisionARViewController+ARManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A006F49231546250097FADF /* VisionARViewController+ARManager.swift */; }; @@ -144,6 +145,7 @@ 2EDFFD72224BBD3E003BB718 /* ObservableVideoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableVideoSource.swift; sourceTree = ""; }; 2EFF563E210B4A5B00B8D512 /* VideoTrimmer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoTrimmer.swift; sourceTree = ""; }; 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheckerTests.swift; sourceTree = ""; }; + 3A2B80B9239E4AE800166F2E /* ObservableVideoSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableVideoSourceTests.swift; sourceTree = ""; }; 3A7E992C22A0299A005EB875 /* MapboxVisionARNative.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MapboxVisionARNative.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AAB2EFA22858F28000D052E /* UIDeviceStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceStubs.swift; sourceTree = ""; }; 5A006F49231546250097FADF /* VisionARViewController+ARManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VisionARViewController+ARManager.swift"; sourceTree = ""; }; @@ -445,6 +447,7 @@ B51E557D215E313500650DDE /* Info.plist */, 2E1FC07A22D4BE1A009BEBFB /* RecordingPathTests.swift */, 5A3E42C422B280180035A010 /* VisionManagerTests.swift */, + 3A2B80B9239E4AE800166F2E /* ObservableVideoSourceTests.swift */, ); path = MapboxVisionTests; sourceTree = ""; @@ -778,6 +781,7 @@ 3A11FEDF2282BCD300B68E5B /* DeviceCheckerTests.swift in Sources */, 3A7676AE22D647FE00ABC483 /* DeviceChecker.swift in Sources */, 5A3C9D9122C39F5300700DC6 /* MockDataProvider.swift in Sources */, + 3A2B80BA239E4AE800166F2E /* ObservableVideoSourceTests.swift in Sources */, 5A3C9D9922C50D0000700DC6 /* MockNative.swift in Sources */, 2E2B828623058986001494E5 /* FileManager+Helpers.swift in Sources */, 5A3C9D8722C37AF700700DC6 /* MockVideoSource.swift in Sources */, diff --git a/MapboxVisionTests/ObservableVideoSourceTests.swift b/MapboxVisionTests/ObservableVideoSourceTests.swift new file mode 100644 index 00000000..80de8c5e --- /dev/null +++ b/MapboxVisionTests/ObservableVideoSourceTests.swift @@ -0,0 +1,27 @@ +// + +import XCTest + +class ObservableVideoSourceTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From 411f5d063173a0a27edf20071b02c695909eceb9 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Thu, 12 Dec 2019 14:51:09 +0300 Subject: [PATCH 25/39] Removed unused parts of Xcode scheme. --- .../xcschemes/MapboxVision.xcscheme | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/MapboxVision.xcodeproj/xcshareddata/xcschemes/MapboxVision.xcscheme b/MapboxVision.xcodeproj/xcshareddata/xcschemes/MapboxVision.xcscheme index a2409141..bbcc286d 100644 --- a/MapboxVision.xcodeproj/xcshareddata/xcschemes/MapboxVision.xcscheme +++ b/MapboxVision.xcodeproj/xcshareddata/xcschemes/MapboxVision.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -39,17 +48,6 @@ - - - - - - - - Date: Thu, 12 Dec 2019 14:52:16 +0300 Subject: [PATCH 26/39] Fix MockFrameRecorder to run unit tests (implementation was changed). --- MapboxVisionTests/Mocks/MockFrameRecorder.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MapboxVisionTests/Mocks/MockFrameRecorder.swift b/MapboxVisionTests/Mocks/MockFrameRecorder.swift index 92685d28..8b67db53 100644 --- a/MapboxVisionTests/Mocks/MockFrameRecorder.swift +++ b/MapboxVisionTests/Mocks/MockFrameRecorder.swift @@ -2,6 +2,10 @@ import Foundation @testable import MapboxVision class MockFrameRecorder: NSObject, FrameRecorder { + func stopRecording() { + actionLog.append(.stopRecording) + } + enum Action: Equatable { case startRecording case stopRecording @@ -14,10 +18,6 @@ class MockFrameRecorder: NSObject, FrameRecorder { actionLog.append(.startRecording) } - func stopRecording(completion: (() -> Void)?) { - actionLog.append(.stopRecording) - } - func handle(frame: CMSampleBuffer) { actionLog.append(.handleFrame) } From e620683e44f004a9ed4569f25c12e5f241369fc8 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Thu, 12 Dec 2019 15:34:28 +0300 Subject: [PATCH 27/39] Update ObservableVideoSource to avoid race condition while we call public methods. --- .../VideoSource/ObservableVideoSource.swift | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift index 7a739f85..2edcc059 100644 --- a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift +++ b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift @@ -6,24 +6,50 @@ import Foundation You may inherit your video source from this class to avoid handling observers yourself. */ open class ObservableVideoSource: NSObject, VideoSource { + // MARK: - Open properties + /// Provides default value of `isExternal` parameter. /// Override if your video source is represented by a module separate from the device. open var isExternal = true + // MARK: - Private properties + + private var observations = [ObjectIdentifier: Observation]() + private var lock: UnsafeMutablePointer + + // MARK: - Lifecycle + + override public init() { + lock = UnsafeMutablePointer.allocate(capacity: 1) + lock.initialize(to: os_unfair_lock()) + } + + deinit { + lock.deallocate() + } + + // MARK: - Open functions + /// :nodoc: open func add(observer: VideoSourceObserver) { + os_unfair_lock_lock(lock) let id = ObjectIdentifier(observer) observations[id] = Observation(observer: observer) + os_unfair_lock_unlock(lock) } /// :nodoc: open func remove(observer: VideoSourceObserver) { + os_unfair_lock_lock(lock) let id = ObjectIdentifier(observer) observations.removeValue(forKey: id) + os_unfair_lock_unlock(lock) } + // MARK: - Public functions /// Use this method to notify all observers about newly available `VideoSample` or `CameraParameters`. public func notify(_ closure: (VideoSourceObserver) -> Void) { + os_unfair_lock_lock(lock) observations.forEach { id, observation in guard let observer = observation.observer else { observations.removeValue(forKey: id) @@ -31,11 +57,10 @@ open class ObservableVideoSource: NSObject, VideoSource { } closure(observer) } + os_unfair_lock_unlock(lock) } +} - private struct Observation { - weak var observer: VideoSourceObserver? - } - - private var observations = [ObjectIdentifier: Observation]() +private struct Observation { + weak var observer: VideoSourceObserver? } From 40a3e1c694dec642e57a3319433619c2c9eb9a41 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Thu, 12 Dec 2019 15:34:45 +0300 Subject: [PATCH 28/39] Implement ObservableVideoSourceTests. --- .../ObservableVideoSourceTests.swift | 127 ++++++++++++++++-- 1 file changed, 115 insertions(+), 12 deletions(-) diff --git a/MapboxVisionTests/ObservableVideoSourceTests.swift b/MapboxVisionTests/ObservableVideoSourceTests.swift index 80de8c5e..315e4a47 100644 --- a/MapboxVisionTests/ObservableVideoSourceTests.swift +++ b/MapboxVisionTests/ObservableVideoSourceTests.swift @@ -1,27 +1,130 @@ -// - +@testable import MapboxVision import XCTest +private enum Constants { + static let numberOfConcurrentMethodCalls = 10000 + static let expectedTestTimeout = 5.0 +} + class ObservableVideoSourceTests: XCTestCase { + private var observableVideoSource: ObservableVideoSource! + private var callsCounter: Int! + + private var testExpectation: XCTestExpectation! + + private var concurrentQueue = DispatchQueue(label: "com.mapbox.MapboxVision.ObservableVideoSourceTests.concurrentQueue", + qos: .default, + attributes: .concurrent) + private var serialQueue = DispatchQueue(label: "com.mapbox.MapboxVision.ObservableVideoSourceTests.serialQueue", + qos: .default, + attributes: []) override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. + super.setUp() + self.observableVideoSource = ObservableVideoSource() } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + // MARK: - Tests + + func testAddMethodDoesNotThrowWhenIsCalledSerially() { + // Given state from setUp() + + // When + for _ in 1...Constants.numberOfConcurrentMethodCalls { + let observer = VideoSourceObserverMock() + + // Then + XCTAssertNoThrow(observableVideoSource.add(observer: observer)) + } } - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. + func testRemoveMethodDoesNotThrowWhenIsCalledSerially() { + // Given state from setUp() + + // When + for _ in 1...Constants.numberOfConcurrentMethodCalls { + let observer = VideoSourceObserverMock() + + // Then + XCTAssertNoThrow(observableVideoSource.add(observer: observer)) + } } - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + func testNotifyMethodDoesNotThrowWhenIsCalledSerially() { + // Given state from setUp() + + // When + for _ in 1...Constants.numberOfConcurrentMethodCalls { + // Then + XCTAssertNoThrow(observableVideoSource.notify { _ in }) } } + func testAddAndNotifyMethodsDoNotThrowWhenAreCalledInParallel() { + // Given + testExpectation = XCTestExpectation(description: "add(:) and notify() methods handle concurrent method calls properly.") + callsCounter = 1 + + // When + for idx in 1...Constants.numberOfConcurrentMethodCalls { + if idx.isMultiple(of: 2) { + concurrentQueue.async { + let observer = VideoSourceObserverMock() + XCTAssertNoThrow(self.observableVideoSource.add(observer: observer)) + self.incrementCallsCounter() + } + } else { + concurrentQueue.async { + XCTAssertNoThrow(self.observableVideoSource.notify { _ in }) + self.incrementCallsCounter() + } + } + } + + // Then + wait(for: [testExpectation], timeout: Constants.expectedTestTimeout) + } + + func testRemoveAndNotifyMethodsWontCrashWhenAreCalledInParallel() { + // Given + testExpectation = XCTestExpectation(description: "remove(:) and notify() methods handle concurrent method calls properly.") + callsCounter = 1 + + for _ in 1...Constants.numberOfConcurrentMethodCalls { + let observer = VideoSourceObserverMock() + observableVideoSource.add(observer: observer) + } + + // When + for idx in 1...Constants.numberOfConcurrentMethodCalls { + if idx.isMultiple(of: 2) { + concurrentQueue.async { + let observer = VideoSourceObserverMock() + XCTAssertNoThrow(self.observableVideoSource.remove(observer: observer)) + self.incrementCallsCounter() + } + } else { + concurrentQueue.async { + XCTAssertNoThrow(self.observableVideoSource.notify { _ in }) + self.incrementCallsCounter() + } + } + } + + // Then + wait(for: [testExpectation], timeout: Constants.expectedTestTimeout) + } + + // MARK: - Helper functions + + private func incrementCallsCounter() { + serialQueue.sync { + callsCounter += 1 + if callsCounter == Constants.numberOfConcurrentMethodCalls { + testExpectation.fulfill() + } + } + } } + +private class VideoSourceObserverMock: VideoSourceObserver {} From 4126c01b946f4bd937a505b1d59cbed6e3eb36ec Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Thu, 12 Dec 2019 15:36:33 +0300 Subject: [PATCH 29/39] Update CHANGELOG.md file. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2542d50..dc893661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Vision - Removed location update in background - Fixed a crash on receiving location updates while stopping `VisionManager` +- Fixed a crash due to race condition in `ObservableVideoSource` ### AR From e7521d4ff78ef87f43e55e7de4b0e4042462a5d9 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Fri, 13 Dec 2019 19:16:04 +0300 Subject: [PATCH 30/39] Update project structure. --- MapboxVision.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapboxVision.xcodeproj/project.pbxproj b/MapboxVision.xcodeproj/project.pbxproj index c9d4ce33..60b6adf7 100644 --- a/MapboxVision.xcodeproj/project.pbxproj +++ b/MapboxVision.xcodeproj/project.pbxproj @@ -445,9 +445,9 @@ B50CFBAB2163C8D700C9C5A5 /* Mocks */, 3A11FEDE2282BCD300B68E5B /* DeviceCheckerTests.swift */, B51E557D215E313500650DDE /* Info.plist */, + 3A2B80B9239E4AE800166F2E /* ObservableVideoSourceTests.swift */, 2E1FC07A22D4BE1A009BEBFB /* RecordingPathTests.swift */, 5A3E42C422B280180035A010 /* VisionManagerTests.swift */, - 3A2B80B9239E4AE800166F2E /* ObservableVideoSourceTests.swift */, ); path = MapboxVisionTests; sourceTree = ""; From 21739655a98a6432cd54a5c54d36c8e20b914066 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Fri, 13 Dec 2019 19:45:09 +0300 Subject: [PATCH 31/39] Review fixes. --- .../VideoSource/ObservableVideoSource.swift | 4 ++++ .../ObservableVideoSourceTests.swift | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift index 2edcc059..6fdb4be7 100644 --- a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift +++ b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift @@ -4,6 +4,9 @@ import Foundation Helper class handling observers: storing, releasing, notifying. Observers are held weakly by the instance of the class. You may inherit your video source from this class to avoid handling observers yourself. + + - Important + A deadlock can occur when add/remove methods are called inside the notify's closure. */ open class ObservableVideoSource: NSObject, VideoSource { // MARK: - Open properties @@ -34,6 +37,7 @@ open class ObservableVideoSource: NSObject, VideoSource { open func add(observer: VideoSourceObserver) { os_unfair_lock_lock(lock) let id = ObjectIdentifier(observer) + print("ID: is \(id)") observations[id] = Observation(observer: observer) os_unfair_lock_unlock(lock) } diff --git a/MapboxVisionTests/ObservableVideoSourceTests.swift b/MapboxVisionTests/ObservableVideoSourceTests.swift index 315e4a47..a14a8b47 100644 --- a/MapboxVisionTests/ObservableVideoSourceTests.swift +++ b/MapboxVisionTests/ObservableVideoSourceTests.swift @@ -16,8 +16,7 @@ class ObservableVideoSourceTests: XCTestCase { qos: .default, attributes: .concurrent) private var serialQueue = DispatchQueue(label: "com.mapbox.MapboxVision.ObservableVideoSourceTests.serialQueue", - qos: .default, - attributes: []) + qos: .default) override func setUp() { super.setUp() @@ -40,13 +39,17 @@ class ObservableVideoSourceTests: XCTestCase { func testRemoveMethodDoesNotThrowWhenIsCalledSerially() { // Given state from setUp() + for _ in 1...Constants.numberOfConcurrentMethodCalls { + let observer = VideoSourceObserverMock() + observableVideoSource.add(observer: observer) + } // When for _ in 1...Constants.numberOfConcurrentMethodCalls { let observer = VideoSourceObserverMock() // Then - XCTAssertNoThrow(observableVideoSource.add(observer: observer)) + XCTAssertNoThrow(observableVideoSource.remove(observer: observer)) } } @@ -118,10 +121,10 @@ class ObservableVideoSourceTests: XCTestCase { // MARK: - Helper functions private func incrementCallsCounter() { - serialQueue.sync { - callsCounter += 1 - if callsCounter == Constants.numberOfConcurrentMethodCalls { - testExpectation.fulfill() + serialQueue.async { + self.callsCounter += 1 + if self.callsCounter == Constants.numberOfConcurrentMethodCalls { + self.testExpectation.fulfill() } } } From 5b0cae7bc2c026387de1ed03e82a5560ff8a2827 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Mon, 16 Dec 2019 12:42:38 +0300 Subject: [PATCH 32/39] Review fixes. Update doc comments. --- .../VideoSource/ObservableVideoSource.swift | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift index 6fdb4be7..c2f5f2fd 100644 --- a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift +++ b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift @@ -5,8 +5,8 @@ import Foundation Observers are held weakly by the instance of the class. You may inherit your video source from this class to avoid handling observers yourself. - - Important - A deadlock can occur when add/remove methods are called inside the notify's closure. + - Warning: + The implementation uses a recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from `notify` closure. */ open class ObservableVideoSource: NSObject, VideoSource { // MARK: - Open properties @@ -33,16 +33,35 @@ open class ObservableVideoSource: NSObject, VideoSource { // MARK: - Open functions - /// :nodoc: + /** + Adds an entry to the list of observers. + + The method is a thread-safe. + + - Parameters: + - observer: Object registering as an observer. + + - Warning: + The implementation uses a recursive lock, thus you must not call this method from `notify(closure:)` method's closure. + */ open func add(observer: VideoSourceObserver) { os_unfair_lock_lock(lock) let id = ObjectIdentifier(observer) - print("ID: is \(id)") observations[id] = Observation(observer: observer) os_unfair_lock_unlock(lock) } - /// :nodoc: + /** + Removes matching entry from the list of observers. + + The method is a thread-safe. + + - Parameters: + - observer: Object registering as an observer. + + - Warning: + The implementation uses a recursive lock, thus you must not call this method inside `notify(closure:)`method's closure. + */ open func remove(observer: VideoSourceObserver) { os_unfair_lock_lock(lock) let id = ObjectIdentifier(observer) @@ -51,7 +70,18 @@ open class ObservableVideoSource: NSObject, VideoSource { } // MARK: - Public functions - /// Use this method to notify all observers about newly available `VideoSample` or `CameraParameters`. + + /** + Use this method to notify all observers about newly available `VideoSample` or `CameraParameters`. + + The method is a thread-safe. + + - Parameters: + - closure: Closure that is called for each entry from the list of observers. It has a reference to a current observer as a parameter. + + - Warning: + The implementation uses a recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from closure. + */ public func notify(_ closure: (VideoSourceObserver) -> Void) { os_unfair_lock_lock(lock) observations.forEach { id, observation in From 9619e4810e10c901d1ef799d8d1831c4dc4c3d10 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Mon, 16 Dec 2019 13:15:41 +0300 Subject: [PATCH 33/39] Review fixes: improve unit tests. --- .../ObservableVideoSourceTests.swift | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/MapboxVisionTests/ObservableVideoSourceTests.swift b/MapboxVisionTests/ObservableVideoSourceTests.swift index a14a8b47..396f4766 100644 --- a/MapboxVisionTests/ObservableVideoSourceTests.swift +++ b/MapboxVisionTests/ObservableVideoSourceTests.swift @@ -39,17 +39,18 @@ class ObservableVideoSourceTests: XCTestCase { func testRemoveMethodDoesNotThrowWhenIsCalledSerially() { // Given state from setUp() - for _ in 1...Constants.numberOfConcurrentMethodCalls { - let observer = VideoSourceObserverMock() - observableVideoSource.add(observer: observer) + + let observers = [VideoSourceObserverMock].init(repeating: VideoSourceObserverMock(), + count: Constants.numberOfConcurrentMethodCalls) + + for idx in 0.. Date: Mon, 16 Dec 2019 16:53:03 +0300 Subject: [PATCH 34/39] Fix a typo in a doc comments. Improve the implementation of unfairLock. --- .../Services/VideoSource/ObservableVideoSource.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift index c2f5f2fd..b4c684e8 100644 --- a/MapboxVision/Services/VideoSource/ObservableVideoSource.swift +++ b/MapboxVision/Services/VideoSource/ObservableVideoSource.swift @@ -6,7 +6,7 @@ import Foundation You may inherit your video source from this class to avoid handling observers yourself. - Warning: - The implementation uses a recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from `notify` closure. + The implementation uses a non-recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from `notify` closure. */ open class ObservableVideoSource: NSObject, VideoSource { // MARK: - Open properties @@ -23,11 +23,12 @@ open class ObservableVideoSource: NSObject, VideoSource { // MARK: - Lifecycle override public init() { - lock = UnsafeMutablePointer.allocate(capacity: 1) + lock = .allocate(capacity: 1) lock.initialize(to: os_unfair_lock()) } deinit { + lock.deinitialize(count: 1) lock.deallocate() } @@ -42,7 +43,7 @@ open class ObservableVideoSource: NSObject, VideoSource { - observer: Object registering as an observer. - Warning: - The implementation uses a recursive lock, thus you must not call this method from `notify(closure:)` method's closure. + The implementation uses a non-recursive lock, thus you must not call this method from `notify(closure:)` method's closure. */ open func add(observer: VideoSourceObserver) { os_unfair_lock_lock(lock) @@ -60,7 +61,7 @@ open class ObservableVideoSource: NSObject, VideoSource { - observer: Object registering as an observer. - Warning: - The implementation uses a recursive lock, thus you must not call this method inside `notify(closure:)`method's closure. + The implementation uses a non-recursive lock, thus you must not call this method inside `notify(closure:)`method's closure. */ open func remove(observer: VideoSourceObserver) { os_unfair_lock_lock(lock) @@ -80,7 +81,7 @@ open class ObservableVideoSource: NSObject, VideoSource { - closure: Closure that is called for each entry from the list of observers. It has a reference to a current observer as a parameter. - Warning: - The implementation uses a recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from closure. + The implementation uses a non-recursive lock, thus you must not call `add(observer:)` or `remove(observer:)` methods from closure. */ public func notify(_ closure: (VideoSourceObserver) -> Void) { os_unfair_lock_lock(lock) From 5510009d61648463fedef093381e53736ff3dd0f Mon Sep 17 00:00:00 2001 From: Alexander Pristavko Date: Wed, 18 Dec 2019 23:43:56 +0800 Subject: [PATCH 35/39] Send turnstile event on changing base url --- MapboxVision/Services/EventsManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MapboxVision/Services/EventsManager.swift b/MapboxVision/Services/EventsManager.swift index 1497098b..76c85c24 100644 --- a/MapboxVision/Services/EventsManager.swift +++ b/MapboxVision/Services/EventsManager.swift @@ -34,6 +34,7 @@ final class EventsManager { extension EventsManager: NetworkClient { func set(baseURL: URL?) { manager.baseURL = baseURL + manager.sendTurnstileEvent() } func sendEvent(name: String, entries: [String: Any]) { From c6cdd93bfbb0b10d9d4d95a2a1b25e6824bce3dc Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Wed, 18 Dec 2019 20:10:20 +0300 Subject: [PATCH 36/39] Adjust api for AR Fence --- .../VIsionARManager/VisionARManager.swift | 15 ++++++++++++++- .../VIsionARManager/VisionARManagerDelegate.swift | 8 +++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/MapboxVisionAR/VIsionARManager/VisionARManager.swift b/MapboxVisionAR/VIsionARManager/VisionARManager.swift index 31e9b52d..bb9fdc83 100644 --- a/MapboxVisionAR/VIsionARManager/VisionARManager.swift +++ b/MapboxVisionAR/VIsionARManager/VisionARManager.swift @@ -18,7 +18,7 @@ public final class VisionARManager { - Parameter visionManager: Instance of `VisionManager`. - - Returns: Instance of `VisionARManager` configured with `VisionManager` instance and delegate. + - Returns: Instance of `VisionARManager` configured with `VisionManager` instance. */ public static func create(visionManager: VisionManagerProtocol) -> VisionARManager { let manager = VisionARManager() @@ -36,6 +36,15 @@ public final class VisionARManager { native?.setLaneLength(laneLength) } + /** + Set AR fence visibility distance in meters. + + - Parameter fenceVisibilityDistance: fence visibility distance in meters. + */ + public func set(fenceVisibilityDistance distance: Float) { + native?.setFenceVisibilityDistance(distance) + } + /** Cleanup the state and resources of `VisionARManger`. */ @@ -76,4 +85,8 @@ extension VisionARManager: VisionARDelegate { public func onARLaneCutoffUpdated(_ cutoff: Float) { delegate?.visionARManager(self, didUpdateARLaneCutoff: cutoff) } + + public func onARFencesUpdated(_ fences: [ARFence]) { + delegate?.visionARManager(self, didUpdateARFences: fences) + } } diff --git a/MapboxVisionAR/VIsionARManager/VisionARManagerDelegate.swift b/MapboxVisionAR/VIsionARManager/VisionARManagerDelegate.swift index ffeb5d2a..88c532d6 100644 --- a/MapboxVisionAR/VIsionARManager/VisionARManagerDelegate.swift +++ b/MapboxVisionAR/VIsionARManager/VisionARManagerDelegate.swift @@ -2,7 +2,6 @@ import Foundation /** Interface that user’s custom object should conform to in order to receive events from `VisionARManager`. - Delegate methods are called one by one followed by `visionManagerDidCompleteUpdate` call on a delegate of `VisionManager`. - NOTE: All delegate methods are called on a background thread. */ @@ -30,6 +29,11 @@ public protocol VisionARManagerDelegate: AnyObject { AR lane cutoff is a relative distance where AR lane should be cut off. Range [0..1] is appropriate. Values greatest than `1` have no effect. */ func visionARManager(_ visionARManager: VisionARManager, didUpdateARLaneCutoff: Float) + + /** + AR fences were updated + */ + func visionARManager(_ visionARManager: VisionARManager, didUpdateARFences arFences: [ARFence]) } public extension VisionARManagerDelegate { @@ -40,4 +44,6 @@ public extension VisionARManagerDelegate { func visionARManager(_ visionARManager: VisionARManager, didUpdateARMask: Image) {} func visionARManager(_ visionARManager: VisionARManager, didUpdateARLaneCutoff: Float) {} + + func visionARManager(_ visionARManager: VisionARManager, didUpdateARFences arFences: [ARFence]) {} } From 47fa9c7abde5edd3becbdf3d5a84e39492117c5a Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Thu, 26 Dec 2019 19:17:39 +0300 Subject: [PATCH 37/39] Fix freeze on init --- CHANGELOG.md | 1 + MapboxVision/Services/VideoSource/CameraVideoSource.swift | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc893661..ad2d675b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Removed location update in background - Fixed a crash on receiving location updates while stopping `VisionManager` - Fixed a crash due to race condition in `ObservableVideoSource` +- Fixed freeze on init ### AR diff --git a/MapboxVision/Services/VideoSource/CameraVideoSource.swift b/MapboxVision/Services/VideoSource/CameraVideoSource.swift index dddd5dec..42561986 100644 --- a/MapboxVision/Services/VideoSource/CameraVideoSource.swift +++ b/MapboxVision/Services/VideoSource/CameraVideoSource.swift @@ -77,14 +77,13 @@ open class CameraVideoSource: ObservableVideoSource { cameraSession.addOutput(dataOutput) } - enableCameraIntrinsicMatrixDelivery() - cameraSession.commitConfiguration() let queue = DispatchQueue(label: "com.mapbox.videoQueue") dataOutput.setSampleBufferDelegate(self, queue: queue) self.dataOutput = dataOutput + enableCameraIntrinsicMatrixDelivery() } private func getCameraParameters(sampleBuffer: CMSampleBuffer) -> CameraParameters? { From ab2a9b4d035bc49a3c056f1045827bbadbff2802 Mon Sep 17 00:00:00 2001 From: Mikhail Ioda Date: Wed, 8 Jan 2020 13:15:59 +0300 Subject: [PATCH 38/39] Review fixes --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2d675b..dc893661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ - Removed location update in background - Fixed a crash on receiving location updates while stopping `VisionManager` - Fixed a crash due to race condition in `ObservableVideoSource` -- Fixed freeze on init ### AR From 26fe1a13ef308dee3f9aa6239470a402cb903765 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 22 Jan 2020 15:19:33 +0300 Subject: [PATCH 39/39] Update changelog --- CHANGELOG.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc893661..062ce7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,30 @@ # Changelog -## 0.11.0 - Unreleased +## 0.11.0 ### Vision +- Added `Germany` country +- Added new `VisionUtils.isVisionSupported` method that checks whether Vision SDK is supported and can be run on the current device +- Added new `SignTypes`: + `InformationCarWashing`, `InformationBusStop`, `RegulatoryPedestriansCrossingUp`, + `RegulatoryPedestriansCrossingDown`, `InformationAutoService`, `InformationFood`, + `InformationTown`, `InformationTownEnd`, `RegulatoryControl`, + `RegulatoryDoubleUTurn`, `SpeedLimitZone`, `SpeedLimitEndZone` +- Changed methods `createCVPixelBuffer` and `createCGImage` on the `Image` class to return retained values instead of `Unmanaged` +- Updated EU classifier +- Improved performance on iPhone 11 family - Removed location update in background -- Fixed a crash on receiving location updates while stopping `VisionManager` -- Fixed a crash due to race condition in `ObservableVideoSource` +- Fixed a crash with `ReachabilityCallback` +- Fixed a crash on receiving location updates while stopping VisionManager +- Fixed a crash due to race condition in ObservableVideoSource +- Fixed a crash happening on `VisionManager.destroy` ### AR - -### Safety +- Added new `Fence` AR style. May be enabled via `VisionARViewController.isFenceVisible` property +- Added `FenceVisualParams` class and `VisionARViewController.setFenceVisualParams` method for customization of `Fence` rendering +- Added `VisionARViewController.setArQuality` method to set overall quality of AR objects +- Added `VisionARViewController.isFenceVisible` and `VisionARViewController.isLaneVisible` to manage displayed AR features +- Fixed issues with AR not shown on some devices ## 0.10.1