diff --git a/ORBIT Camera.xcodeproj/project.pbxproj b/ORBIT Camera.xcodeproj/project.pbxproj index b4cd517..a74d32d 100644 --- a/ORBIT Camera.xcodeproj/project.pbxproj +++ b/ORBIT Camera.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 4BE50EF72438879E00BE79C7 /* ThingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE50EF62438879E00BE79C7 /* ThingCell.swift */; }; 4BE77689247910470088562D /* Video+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE77688247910470088562D /* Video+ServerStatus.swift */; }; 4BE7768C247A98360088562D /* Participant+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE7768B247A98350088562D /* Participant+ServerStatus.swift */; }; + 4BE7768F247ABEB40088562D /* orbit-cup-photoreal.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */; }; 4BF14BBA2405730700E31D93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BB92405730700E31D93 /* SceneDelegate.swift */; }; 4BF14BBC2405730700E31D93 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBB2405730700E31D93 /* MasterViewController.swift */; }; 4BF14BBE2405730700E31D93 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBD2405730700E31D93 /* DetailViewController.swift */; }; @@ -117,6 +118,7 @@ 4BE50EF42437F07C00BE79C7 /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = ""; }; 4BE50EF62438879E00BE79C7 /* ThingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThingCell.swift; sourceTree = ""; }; 4BE77688247910470088562D /* Video+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+ServerStatus.swift"; sourceTree = ""; }; + 4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = "orbit-cup-photoreal.mp4"; sourceTree = ""; }; 4BE7768B247A98350088562D /* Participant+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+ServerStatus.swift"; sourceTree = ""; }; 4BF14BB42405730700E31D93 /* ORBIT Camera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ORBIT Camera.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4BF14BB72405730700E31D93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -234,6 +236,7 @@ 4BFC5F222458B12800E5A3C1 /* InformedConsent.markdown */, 4BFC5F28245B6BD600E5A3C1 /* Introduction.markdown */, 4B3119AC2450977100E28452 /* Recording.markdown */, + 4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */, 4BF14BC22405730900E31D93 /* Assets.xcassets */, 4BF14BC42405730900E31D93 /* LaunchScreen.storyboard */, 4BF14BC72405730900E31D93 /* Info.plist */, @@ -350,6 +353,7 @@ 4B3119AE2450977100E28452 /* Recording.markdown in Resources */, 4BF14BC32405730900E31D93 /* Assets.xcassets in Resources */, 4BF14BC12405730700E31D93 /* Main.storyboard in Resources */, + 4BE7768F247ABEB40088562D /* orbit-cup-photoreal.mp4 in Resources */, 4BFC5F212458ACE200E5A3C1 /* ParticipantInformation.markdown in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ORBIT Camera/DetailViewController.swift b/ORBIT Camera/DetailViewController.swift index 05617ed..0cffa6b 100644 --- a/ORBIT Camera/DetailViewController.swift +++ b/ORBIT Camera/DetailViewController.swift @@ -596,27 +596,20 @@ class DetailViewController: UIViewController { os_log("Could not get video for re-record recordStart", log: appUILog) return } - do { - try FileManager.default.removeItem(at: video.url) - } catch { - os_log("Could not delete previous recording to re-record", log: appUILog) - return - } // Go, configuring completion handler that updates the UI + let url = Video.mintRecordURL() let videoPageIndex = pageIndex - camera.recordStart(to: video.url) { [weak self] in + camera.recordStart(to: url) { [weak self] in guard let self = self else { return } // Update controller state self.rerecordPageIndexes.remove(videoPageIndex) - // Delete server record - video.deleteUpload() - video.orbitID = nil - // Update record + video.rerecordReset() + video.url = url video.recorded = Date() try! dbQueue.write { db in try video.save(db) } @@ -628,16 +621,9 @@ class DetailViewController: UIViewController { os_log("No thing on recordStart", log: appUILog) return } - guard let url = try? FileManager.default - .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - .appendingPathComponent(NSUUID().uuidString) - .appendingPathExtension("mov") - else { - os_log("Could not create URL for recordStart", log: appUILog) - return - } // Go, setting completion handler that creates a Video record and updates the UI + let url = Video.mintRecordURL() let kind = videoKind(description: videoPageControl.currentCategoryName!) camera.recordStart(to: url) { // Create a Video record diff --git a/ORBIT Camera/Video.swift b/ORBIT Camera/Video.swift index dc03ef6..9fd71bf 100644 --- a/ORBIT Camera/Video.swift +++ b/ORBIT Camera/Video.swift @@ -19,10 +19,28 @@ struct Video: Codable, Equatable { var thingID: Int64? /// On-device file URL of a video the participant has recorded - /// As the app's data folder is named dynamically, only store the filename and get/set the URL relative to a type set storage location + /// To minimise iCloud backup size and low-storage scenarios, the video file will be moved to the cache folder when upload is complete. + /// If the system then culls the video file from the cache folder, a bundled-with-the-app placeholder video URL will be returned instead. + /// Plus, as the app's data folder is named dynamically, only store the filename and get/set the URL relative to a type set storage location var url: URL { - get { URL(fileURLWithPath: filename, relativeTo: Video.storageURL) } - set { filename = newValue.lastPathComponent } + get { + var url = URL(fileURLWithPath: filename, relativeTo: Video.storageURL) + do { + try _ = url.checkResourceIsReachable() + return url + } + catch {} + url = URL(fileURLWithPath: filename, relativeTo: Video.storageCacheURL) + do { + try _ = url.checkResourceIsReachable() + return url + } + catch {} + return Video.placeholderURL + } + set { + filename = newValue.lastPathComponent + } } /// When the video was recorded @@ -30,7 +48,9 @@ struct Video: Codable, Equatable { /// A unique ID for the thing in the ORBIT dataset (or rather, the database the dataset will be produced from) // Note this was handled more elegantly by orbitID being an UploadStatus enum, but the supporting code was getting ridiculous. - var orbitID: Int? + var orbitID: Int? { + didSet { if oldValue == nil && orbitID != nil { moveToCacheStorage() } } + } /// The kind of video this is. /// Current terminology: videos are taken with one of two goals: "train" or "test", with two "techniques" used for test videos: "zoom" and "pan". @@ -96,23 +116,38 @@ struct Video: Codable, Equatable { self.url = url } - // Private property backing `url` - private var filename: String - - // Private type property backing `url` - private static var storageURL: URL { - try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try! - } - - // Private type property backing `url` - private static var storageCacheURL: URL { - try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try! + /// Reset in preparation for a new video URL to be set. Deletes the file, deletes the server record, resets statuses pertaining to previous video + mutating func rerecordReset() { + // Cancel any in-progress upload + cancelUploading() + + // Delete video from server + deleteUpload() + + // Remove file + for url in [URL(fileURLWithPath: filename, relativeTo: Video.storageURL), URL(fileURLWithPath: filename, relativeTo: Video.storageURL)] { + do { + try FileManager.default.removeItem(at: url) + break + } catch {} + } + + // Reset statuses + verified = .unvalidated + orbitID = nil } - private static var placeholderURL: URL { - Bundle.main.url(forResource: "orbit-cup-photoreal", withExtension: "mp4")! // FIXME: ! + /// Generate a URL suitable for recording a video and then setting this URL as the video's property + static func mintRecordURL() -> URL { + Video.storageURL + .appendingPathComponent(NSUUID().uuidString) + .appendingPathExtension("mov") } + + // Private property backing `url` + private var filename: String + // Private method backing `url` private func moveToCacheStorage() { do { try FileManager.default.moveItem( @@ -120,9 +155,18 @@ struct Video: Codable, Equatable { to: URL(fileURLWithPath: filename, relativeTo: Video.storageCacheURL) ) } catch { - print(error) + os_log("Move of %{public}s file to cache failed", self.description) } } + + // Private type property backing `url` + private static let storageURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try! + + // Private type property backing `url` + private static let storageCacheURL = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try! + + // Private type property backing `url` + private static let placeholderURL = Bundle.main.url(forResource: "orbit-cup-photoreal", withExtension: "mp4")! // FIXME: ! } extension Video: FetchableRecord, MutablePersistableRecord { @@ -152,7 +196,14 @@ extension Video: FetchableRecord, MutablePersistableRecord { deleteUpload() // Remove file - try FileManager.default.removeItem(at: url) + for url in [URL(fileURLWithPath: filename, relativeTo: Video.storageURL), URL(fileURLWithPath: filename, relativeTo: Video.storageURL)] { + do { + try FileManager.default.removeItem(at: url) + break + } catch {} + } + + // Delete record let deleted = try performDelete(db) if !deleted { os_log("Failed to delete Video") } return deleted diff --git a/ORBIT Camera/orbit-cup-photoreal.mp4 b/ORBIT Camera/orbit-cup-photoreal.mp4 new file mode 100644 index 0000000..3e9b19f Binary files /dev/null and b/ORBIT Camera/orbit-cup-photoreal.mp4 differ