Skip to content

Commit 82709fb

Browse files
authored
Releases/v0.4.0 (#75)
* Project Input standardization (#17) (#41) (#46) (#48) (#57) (#77) Add AVFoundation and PhotosKit initializers Add internal and external state mapping Remove duplicate status enum and add inline docs to external status Add inline API docs to PHAsset-based MuxUpload constructor Consolidate all `MuxUpload` options into a single struct `UploadOptions` Declare asynchronous MuxUpload constructor in PHAsset extension Place extension methods into dedicated directories Polish inline API documentation Add new API documentation and note the placeholder implementation Add option variants as static members: defaults, disabled inputStandardization Deprecate existing initializer, normally this API should be removed prior to GA, but since it was the only initializer exposed up to this point removing it would break everybody. Instead deprecate and remove at a later date. Store all MuxUpload-related options in UploadInfo Use correct starting byte parameter when restarting upload If input standardized, standardized input URL is passed to UploadInfo instead of the original input URL used for initializer Note: SDK probably needs to re-export a high quality asset anyway so possibly need a bridging status Add dedicated internal initializer for MuxUpload error with unknown error code Request local and remote assets Standardize via AVFoundation asset export session Expose hook for client to cancel upload if standardization failed Call cancellation hook if inspection fails. We're not sure if the input is standard or not so better to be safe and confirm Export based on maximum resolution set by client Cleaner non standard input handler invocation Add CustomStringConvertible conformance to maximum resolution (#56) Only mark upload as started if its ready Safe storage for MuxUpload (#71) Intended to prevent a crash if MuxUpload is extended by the SDK client to conform to Equatable or Hashable protocols Switch order of operations to avoid long pause on fetching duration AVAsset sometimes hangs when asked to asynchronously fetch duration when there aren't audio or video tracks present. To avoid this after starting the upload, the inspection step will get the video tracks first and get the duration afterwards. --------- Co-authored-by: Emily Dixon <edixon@mux.com> * Minor example app renaming (#29) * Use a UUID string as MuxUpload internal identifier (#30) * Display a more specific error message when the direct upload POST request fails (#32) * Use MuxUpload id instead if the input URL when looking up or writing state in the SDK (#33) * Change upload creation example app method to use discardableResult (#34) * Add dedicated internal initializer for MuxUpload error with unknown error code (#35) * Rename enum and adjust to camel casing (#36) * Adhere to Swift formatting guidelines, remove snake casing (#37) * Fix potential crash in ChunkedFile (#38) * Include Cloud shared asset sources when requesting assets (#40) * Prevent arithmentic overflow when setting chunk content range value (#45) * Remove force unwrap that can cause a crash (#47) * Make internal class methods internal (#51)
1 parent 70d265e commit 82709fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3018
-509
lines changed

Example/SwiftUploadSDKExample/SwiftUploadSDKExample.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@
466466
CODE_SIGN_STYLE = Automatic;
467467
CURRENT_PROJECT_VERSION = 1;
468468
DEVELOPMENT_ASSET_PATHS = "\"SwiftUploadSDKExample/Preview Content\"";
469-
DEVELOPMENT_TEAM = XX95P4Y787;
469+
DEVELOPMENT_TEAM = "";
470470
ENABLE_PREVIEWS = YES;
471471
GENERATE_INFOPLIST_FILE = YES;
472472
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app uploads photos from your camera roll";
@@ -498,7 +498,7 @@
498498
CODE_SIGN_STYLE = Automatic;
499499
CURRENT_PROJECT_VERSION = 1;
500500
DEVELOPMENT_ASSET_PATHS = "\"SwiftUploadSDKExample/Preview Content\"";
501-
DEVELOPMENT_TEAM = XX95P4Y787;
501+
DEVELOPMENT_TEAM = "";
502502
ENABLE_PREVIEWS = YES;
503503
GENERATE_INFOPLIST_FILE = YES;
504504
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app uploads photos from your camera roll";

Example/SwiftUploadSDKExample/SwiftUploadSDKExample/Model/ThumbnailModel.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ThumbnailModel: ObservableObject {
5252
private var thumbnailGenerator: AVAssetImageGenerator?
5353

5454
@Published var thumbnail: CGImage?
55-
@Published var uploadProgress: MuxUpload.Status?
55+
@Published var uploadProgress: MuxUpload.TransportStatus?
5656

5757
init(asset: AVAsset, upload: MuxUpload) {
5858
self.asset = asset

Example/SwiftUploadSDKExample/SwiftUploadSDKExample/Model/UploadCreationModel.swift

+16-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import MuxUploadSDK
1111

1212
class UploadCreationModel : ObservableObject {
1313

14-
struct PickerError: Error {
14+
struct PickerError: Error, Equatable {
1515

1616
static var unexpectedFormat: PickerError {
1717
PickerError(localizedDescription: "Unexpected video file format")
@@ -24,6 +24,10 @@ class UploadCreationModel : ObservableObject {
2424
static var createUploadFailed: PickerError {
2525
PickerError(localizedDescription: "Upload could not be created")
2626
}
27+
28+
static var assetExportSessionFailed: PickerError {
29+
PickerError(localizedDescription: "Upload could not be exported")
30+
}
2731

2832
var localizedDescription: String
2933

@@ -37,7 +41,7 @@ class UploadCreationModel : ObservableObject {
3741
}
3842
}
3943

40-
func startUpload(preparedMedia: PreparedUpload, forceRestart: Bool) -> MuxUpload {
44+
@discardableResult func startUpload(preparedMedia: PreparedUpload, forceRestart: Bool) -> MuxUpload {
4145
let upload = MuxUpload(
4246
uploadURL: preparedMedia.remoteURL,
4347
videoFileURL: preparedMedia.localVideoFile
@@ -52,6 +56,10 @@ class UploadCreationModel : ObservableObject {
5256

5357
/// Prepares a Photos Asset for upload by exporting it to a local temp file
5458
func tryToPrepare(from pickerResult: PHPickerResult) {
59+
if case ExportState.preparing = exportState {
60+
return
61+
}
62+
5563
// Cancel anything that was already happening
5664
if let assetRequestId = assetRequestId {
5765
PHImageManager.default().cancelImageRequest(assetRequestId)
@@ -71,11 +79,11 @@ class UploadCreationModel : ObservableObject {
7179

7280
guard let assetIdentitfier = pickerResult.assetIdentifier else {
7381
NSLog("!! No Asset ID for chosen asset")
74-
exportState = .failure(nil)
82+
exportState = .failure(UploadCreationModel.PickerError.assetExportSessionFailed)
7583
return
7684
}
7785
let options = PHFetchOptions()
78-
options.includeAssetSourceTypes = .typeUserLibrary
86+
options.includeAssetSourceTypes = [.typeUserLibrary, .typeCloudShared]
7987
let phAssetResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentitfier], options: options)
8088
guard let phAsset = phAssetResult.firstObject else {
8189
self.logger.error("!! No Asset fetched")
@@ -88,12 +96,13 @@ class UploadCreationModel : ObservableObject {
8896
}
8997

9098
let exportOptions = PHVideoRequestOptions()
91-
//exportOptions.deliveryMode = .highQualityFormat
99+
exportOptions.isNetworkAccessAllowed = true
100+
exportOptions.deliveryMode = .highQualityFormat
92101
assetRequestId = PHImageManager.default().requestExportSession(forVideo: phAsset, options: exportOptions, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: {(exportSession, info) -> Void in
93102
DispatchQueue.main.async {
94103
guard let exportSession = exportSession else {
95104
self.logger.error("!! No Export session")
96-
self.exportState = .failure(nil)
105+
self.exportState = .failure(UploadCreationModel.PickerError.assetExportSessionFailed)
97106
return
98107
}
99108
self.exportToOutFile(session: exportSession, outFile: tempFile)
@@ -207,7 +216,7 @@ struct PreparedUpload {
207216
}
208217

209218
enum ExportState {
210-
case not_started, preparing, failure(UploadCreationModel.PickerError?), ready(PreparedUpload)
219+
case not_started, preparing, failure(UploadCreationModel.PickerError), ready(PreparedUpload)
211220
}
212221

213222
enum PhotosAuthState {

Example/SwiftUploadSDKExample/SwiftUploadSDKExample/Model/UploadListModel.swift

+20-10
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@ class UploadListModel : ObservableObject {
1515
UploadManager.shared.addUploadsUpdatedDelegate(
1616
Delegate(
1717
handler: { uploads in
18-
var uploadSet = Set(self.lastKnownUploads)
19-
uploads.forEach {
20-
uploadSet.insert($0)
21-
}
22-
self.lastKnownUploads = Array(uploadSet)
23-
.sorted(
24-
by: { lhs, rhs in
25-
lhs.uploadStatus.startTime >= rhs.uploadStatus.startTime
18+
19+
var lastKnownUploadsToUpdate = self.lastKnownUploads
20+
21+
for updatedUpload in uploads {
22+
if !lastKnownUploadsToUpdate.contains(
23+
where: {
24+
$0.uploadURL == updatedUpload.uploadURL &&
25+
$0.videoFile == updatedUpload.videoFile
26+
}
27+
) {
28+
lastKnownUploadsToUpdate.append(updatedUpload)
2629
}
27-
)
28-
}
30+
}
31+
32+
self.lastKnownUploads = lastKnownUploadsToUpdate
33+
.sorted(
34+
by: { lhs, rhs in
35+
(lhs.uploadStatus?.startTime ?? 0) >= (rhs.uploadStatus?.startTime ?? 0)
36+
}
37+
)
38+
}
2939
)
3040
)
3141
}

Example/SwiftUploadSDKExample/SwiftUploadSDKExample/Screens/CreateUploadView.swift

+16-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ fileprivate struct ErrorView: View {
5757
.foregroundColor(.red)
5858
Spacer()
5959
.frame(maxHeight: 12)
60-
Text("Couln't prepare the video for upload. Please try another video")
60+
61+
Text(message)
6162
.foregroundColor(White)
6263
.multilineTextAlignment(.center)
6364
.font(.system(size: 12))
@@ -80,9 +81,22 @@ fileprivate struct ErrorView: View {
8081
}
8182

8283
let error: Error?
84+
85+
let message: String
8386

8487
init(error: Error? = nil) {
8588
self.error = error
89+
self.message = "Couldn't prepare the video for upload. Please try another video."
90+
}
91+
92+
init(error: UploadCreationModel.PickerError) {
93+
self.error = error
94+
95+
if error == UploadCreationModel.PickerError.createUploadFailed {
96+
self.message = "Couldn't create direct upload. Check your access token and network connectivity."
97+
} else {
98+
self.message = "Couldn't prepare the video for upload. Please try another video."
99+
}
86100
}
87101
}
88102

@@ -134,7 +148,7 @@ fileprivate struct ThumbnailView: View {
134148
Spacer()
135149
StretchyDefaultButton("Upload") {
136150
if let preparedMedia = preparedMedia {
137-
let upload = uploadCreationVM.startUpload(preparedMedia: preparedMedia, forceRestart: true)
151+
uploadCreationVM.startUpload(preparedMedia: preparedMedia, forceRestart: true)
138152
dismiss()
139153
}
140154
}

Example/SwiftUploadSDKExample/SwiftUploadSDKExample/Screens/UploadListView.swift

+17-11
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,29 @@ struct UploadListScreen: View {
1717
WindowBackground
1818
VStack(spacing: 0) {
1919
MuxNavBar()
20-
ListContianer()
20+
ListContainerView()
2121
}
2222
}
2323
}
2424
}
2525

26-
fileprivate struct ListContianer: View {
26+
extension MuxUpload {
27+
var objectIdentifier: ObjectIdentifier {
28+
ObjectIdentifier(self)
29+
}
30+
}
31+
32+
fileprivate struct ListContainerView: View {
2733

28-
@EnvironmentObject var listVM: UploadListModel
34+
@EnvironmentObject var viewModel: UploadListModel
2935

3036
var body: some View {
31-
if listVM.lastKnownUploads.isEmpty {
37+
if viewModel.lastKnownUploads.isEmpty {
3238
EmptyList()
3339
} else {
3440
ScrollView {
3541
LazyVStack {
36-
ForEach(listVM.lastKnownUploads, id: \.self) { upload in
42+
ForEach(viewModel.lastKnownUploads, id: \.objectIdentifier) { upload in
3743
ListItem(upload: upload)
3844
}
3945
}
@@ -124,12 +130,12 @@ fileprivate struct ListItem: View {
124130
}
125131
}
126132

127-
private func statusLine(status: MuxUpload.Status?) -> String {
128-
guard let status = status, let progress = status.progress, status.startTime > 0 else {
133+
private func statusLine(status: MuxUpload.TransportStatus?) -> String {
134+
guard let status = status, let progress = status.progress, let startTime = status.startTime, startTime > 0 else {
129135
return "missing status"
130136
}
131137

132-
let totalTimeSecs = status.updatedTime - status.startTime
138+
let totalTimeSecs = status.updatedTime - (status.startTime ?? 0)
133139
let totalTimeMs = Int64((totalTimeSecs) * 1000)
134140
let kbytesPerSec = (progress.completedUnitCount) / totalTimeMs // bytes/milli = kb/sec
135141
let fourSigs = NumberFormatter()
@@ -146,7 +152,7 @@ fileprivate struct ListItem: View {
146152
return "\(formattedMBytes) MB in \(formattedTime)s (\(formattedDataRate) KB/s)"
147153
}
148154

149-
private func elapsedBytesOfTotal(status: MuxUpload.Status) -> String {
155+
private func elapsedBytesOfTotal(status: MuxUpload.TransportStatus) -> String {
150156
guard let progress = status.progress else {
151157
return "unknown"
152158
}
@@ -157,7 +163,7 @@ fileprivate struct ListItem: View {
157163
self.upload = upload
158164
_thumbnailModel = StateObject(
159165
wrappedValue: {
160-
ThumbnailModel(asset: AVAsset(url: upload.videoFile), upload: upload)
166+
ThumbnailModel(asset: AVAsset(url: upload.videoFile!), upload: upload)
161167
}()
162168
)
163169
}
@@ -188,7 +194,7 @@ struct ListContent_Previews: PreviewProvider {
188194
static var previews: some View {
189195
ZStack(alignment: .top) {
190196
WindowBackground
191-
ListContianer()
197+
ListContainerView()
192198
}
193199
.environmentObject(UploadListModel())
194200
}

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ let videoInputURL: URL = /* File URL to your video file. See Test App for how to
4040

4141
let upload = MuxUpload(
4242
uploadURL: directUploadURL,
43-
videoFileURL: videoInputURL,
43+
inputFileURL: videoInputURL,
4444
)
4545

4646
upload.progressHandler = { state in
@@ -63,7 +63,7 @@ self.upload = upload
6363
upload.start()
6464
```
6565

66-
A simple example usage can be found in our [Test App](https://github.com/muxinc/swift-upload-sdk/blob/main/apps/Test%20App/Test%20App/Screens/UploadScreenViewModel.swift)
66+
A simple example of how to use the SDK in a realistic app can be found [here](https://github.com/muxinc/swift-upload-sdk/blob/main/Examples/)
6767

6868
## Development
6969

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// Bundle+Reporting.swift
3+
//
4+
5+
import Foundation
6+
7+
extension Bundle {
8+
var appName: String? {
9+
return object(forInfoDictionaryKey: "CFBundleName") as? String
10+
}
11+
12+
var appVersion: String? {
13+
return object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// FileManager+FileOperations.swift
3+
//
4+
5+
import Foundation
6+
7+
extension FileManager {
8+
9+
// Work around Swift compiler not bridging Dictionary
10+
// and NSDictionary properly when calling attributesOfItem
11+
func fileSizeOfItem(
12+
atPath path: String
13+
) throws -> UInt64 {
14+
(try attributesOfItem(atPath: path) as NSDictionary)
15+
.fileSize()
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// NSMutableURLRequest+Reporting.swift
3+
//
4+
5+
import Foundation
6+
7+
extension NSMutableURLRequest {
8+
static func makeJSONPost(
9+
url: URL,
10+
httpBody: Data,
11+
additionalHTTPHeaders: [String: String]
12+
) -> NSMutableURLRequest {
13+
let request = NSMutableURLRequest(
14+
url: url,
15+
cachePolicy: .useProtocolCachePolicy,
16+
timeoutInterval: 10.0
17+
)
18+
19+
request.httpMethod = "POST"
20+
request.setValue("application/json", forHTTPHeaderField: "Accept")
21+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
22+
for keypair in additionalHTTPHeaders {
23+
request.setValue(keypair.value, forHTTPHeaderField: keypair.key)
24+
}
25+
26+
request.httpBody = httpBody
27+
28+
return request
29+
}
30+
}

0 commit comments

Comments
 (0)