Skip to content

Commit

Permalink
Merge pull request #42 from tobyspark/feature-purgeable-video-files
Browse files Browse the repository at this point in the history
Feature: Move video file to purgeable storage once uploaded

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 will be shown instead.
  • Loading branch information
tobyspark authored May 24, 2020
2 parents 821dc12 + 20bc31f commit aff31a3
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 38 deletions.
24 changes: 5 additions & 19 deletions ORBIT Camera/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand All @@ -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
Expand Down
89 changes: 70 additions & 19 deletions ORBIT Camera/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,38 @@ 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
var recorded: Date

/// 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".
Expand Down Expand Up @@ -96,33 +116,57 @@ 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(
at: URL(fileURLWithPath: filename, relativeTo: Video.storageURL),
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 {
Expand Down Expand Up @@ -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
Expand Down
Binary file added ORBIT Camera/orbit-cup-photoreal.mp4
Binary file not shown.

0 comments on commit aff31a3

Please sign in to comment.