Skip to content

Commit

Permalink
Releases/v0.4.0 (#75)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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)
  • Loading branch information
andrewjl-mux authored Jul 21, 2023
1 parent a1654f5 commit fe4407e
Show file tree
Hide file tree
Showing 42 changed files with 3,018 additions and 509 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUploadSDKExample/Preview Content\"";
DEVELOPMENT_TEAM = XX95P4Y787;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app uploads photos from your camera roll";
Expand Down Expand Up @@ -498,7 +498,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUploadSDKExample/Preview Content\"";
DEVELOPMENT_TEAM = XX95P4Y787;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app uploads photos from your camera roll";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ThumbnailModel: ObservableObject {
private var thumbnailGenerator: AVAssetImageGenerator?

@Published var thumbnail: CGImage?
@Published var uploadProgress: MuxUpload.Status?
@Published var uploadProgress: MuxUpload.TransportStatus?

init(asset: AVAsset, upload: MuxUpload) {
self.asset = asset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MuxUploadSDK

class UploadCreationModel : ObservableObject {

struct PickerError: Error {
struct PickerError: Error, Equatable {

static var unexpectedFormat: PickerError {
PickerError(localizedDescription: "Unexpected video file format")
Expand All @@ -24,6 +24,10 @@ class UploadCreationModel : ObservableObject {
static var createUploadFailed: PickerError {
PickerError(localizedDescription: "Upload could not be created")
}

static var assetExportSessionFailed: PickerError {
PickerError(localizedDescription: "Upload could not be exported")
}

var localizedDescription: String

Expand All @@ -37,7 +41,7 @@ class UploadCreationModel : ObservableObject {
}
}

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

/// Prepares a Photos Asset for upload by exporting it to a local temp file
func tryToPrepare(from pickerResult: PHPickerResult) {
if case ExportState.preparing = exportState {
return
}

// Cancel anything that was already happening
if let assetRequestId = assetRequestId {
PHImageManager.default().cancelImageRequest(assetRequestId)
Expand All @@ -71,11 +79,11 @@ class UploadCreationModel : ObservableObject {

guard let assetIdentitfier = pickerResult.assetIdentifier else {
NSLog("!! No Asset ID for chosen asset")
exportState = .failure(nil)
exportState = .failure(UploadCreationModel.PickerError.assetExportSessionFailed)
return
}
let options = PHFetchOptions()
options.includeAssetSourceTypes = .typeUserLibrary
options.includeAssetSourceTypes = [.typeUserLibrary, .typeCloudShared]
let phAssetResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentitfier], options: options)
guard let phAsset = phAssetResult.firstObject else {
self.logger.error("!! No Asset fetched")
Expand All @@ -88,12 +96,13 @@ class UploadCreationModel : ObservableObject {
}

let exportOptions = PHVideoRequestOptions()
//exportOptions.deliveryMode = .highQualityFormat
exportOptions.isNetworkAccessAllowed = true
exportOptions.deliveryMode = .highQualityFormat
assetRequestId = PHImageManager.default().requestExportSession(forVideo: phAsset, options: exportOptions, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: {(exportSession, info) -> Void in
DispatchQueue.main.async {
guard let exportSession = exportSession else {
self.logger.error("!! No Export session")
self.exportState = .failure(nil)
self.exportState = .failure(UploadCreationModel.PickerError.assetExportSessionFailed)
return
}
self.exportToOutFile(session: exportSession, outFile: tempFile)
Expand Down Expand Up @@ -207,7 +216,7 @@ struct PreparedUpload {
}

enum ExportState {
case not_started, preparing, failure(UploadCreationModel.PickerError?), ready(PreparedUpload)
case not_started, preparing, failure(UploadCreationModel.PickerError), ready(PreparedUpload)
}

enum PhotosAuthState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ class UploadListModel : ObservableObject {
UploadManager.shared.addUploadsUpdatedDelegate(
Delegate(
handler: { uploads in
var uploadSet = Set(self.lastKnownUploads)
uploads.forEach {
uploadSet.insert($0)
}
self.lastKnownUploads = Array(uploadSet)
.sorted(
by: { lhs, rhs in
lhs.uploadStatus.startTime >= rhs.uploadStatus.startTime

var lastKnownUploadsToUpdate = self.lastKnownUploads

for updatedUpload in uploads {
if !lastKnownUploadsToUpdate.contains(
where: {
$0.uploadURL == updatedUpload.uploadURL &&
$0.videoFile == updatedUpload.videoFile
}
) {
lastKnownUploadsToUpdate.append(updatedUpload)
}
)
}
}

self.lastKnownUploads = lastKnownUploadsToUpdate
.sorted(
by: { lhs, rhs in
(lhs.uploadStatus?.startTime ?? 0) >= (rhs.uploadStatus?.startTime ?? 0)
}
)
}
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ fileprivate struct ErrorView: View {
.foregroundColor(.red)
Spacer()
.frame(maxHeight: 12)
Text("Couln't prepare the video for upload. Please try another video")

Text(message)
.foregroundColor(White)
.multilineTextAlignment(.center)
.font(.system(size: 12))
Expand All @@ -80,9 +81,22 @@ fileprivate struct ErrorView: View {
}

let error: Error?

let message: String

init(error: Error? = nil) {
self.error = error
self.message = "Couldn't prepare the video for upload. Please try another video."
}

init(error: UploadCreationModel.PickerError) {
self.error = error

if error == UploadCreationModel.PickerError.createUploadFailed {
self.message = "Couldn't create direct upload. Check your access token and network connectivity."
} else {
self.message = "Couldn't prepare the video for upload. Please try another video."
}
}
}

Expand Down Expand Up @@ -134,7 +148,7 @@ fileprivate struct ThumbnailView: View {
Spacer()
StretchyDefaultButton("Upload") {
if let preparedMedia = preparedMedia {
let upload = uploadCreationVM.startUpload(preparedMedia: preparedMedia, forceRestart: true)
uploadCreationVM.startUpload(preparedMedia: preparedMedia, forceRestart: true)
dismiss()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,29 @@ struct UploadListScreen: View {
WindowBackground
VStack(spacing: 0) {
MuxNavBar()
ListContianer()
ListContainerView()
}
}
}
}

fileprivate struct ListContianer: View {
extension MuxUpload {
var objectIdentifier: ObjectIdentifier {
ObjectIdentifier(self)
}
}

fileprivate struct ListContainerView: View {

@EnvironmentObject var listVM: UploadListModel
@EnvironmentObject var viewModel: UploadListModel

var body: some View {
if listVM.lastKnownUploads.isEmpty {
if viewModel.lastKnownUploads.isEmpty {
EmptyList()
} else {
ScrollView {
LazyVStack {
ForEach(listVM.lastKnownUploads, id: \.self) { upload in
ForEach(viewModel.lastKnownUploads, id: \.objectIdentifier) { upload in
ListItem(upload: upload)
}
}
Expand Down Expand Up @@ -124,12 +130,12 @@ fileprivate struct ListItem: View {
}
}

private func statusLine(status: MuxUpload.Status?) -> String {
guard let status = status, let progress = status.progress, status.startTime > 0 else {
private func statusLine(status: MuxUpload.TransportStatus?) -> String {
guard let status = status, let progress = status.progress, let startTime = status.startTime, startTime > 0 else {
return "missing status"
}

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

private func elapsedBytesOfTotal(status: MuxUpload.Status) -> String {
private func elapsedBytesOfTotal(status: MuxUpload.TransportStatus) -> String {
guard let progress = status.progress else {
return "unknown"
}
Expand All @@ -157,7 +163,7 @@ fileprivate struct ListItem: View {
self.upload = upload
_thumbnailModel = StateObject(
wrappedValue: {
ThumbnailModel(asset: AVAsset(url: upload.videoFile), upload: upload)
ThumbnailModel(asset: AVAsset(url: upload.videoFile!), upload: upload)
}()
)
}
Expand Down Expand Up @@ -188,7 +194,7 @@ struct ListContent_Previews: PreviewProvider {
static var previews: some View {
ZStack(alignment: .top) {
WindowBackground
ListContianer()
ListContainerView()
}
.environmentObject(UploadListModel())
}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let videoInputURL: URL = /* File URL to your video file. See Test App for how to

let upload = MuxUpload(
uploadURL: directUploadURL,
videoFileURL: videoInputURL,
inputFileURL: videoInputURL,
)

upload.progressHandler = { state in
Expand All @@ -63,7 +63,7 @@ self.upload = upload
upload.start()
```

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)
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/)

## Development

Expand Down
15 changes: 15 additions & 0 deletions Sources/MuxUploadSDK/Extensions/Bundle+Reporting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Bundle+Reporting.swift
//

import Foundation

extension Bundle {
var appName: String? {
return object(forInfoDictionaryKey: "CFBundleName") as? String
}

var appVersion: String? {
return object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
}
}
17 changes: 17 additions & 0 deletions Sources/MuxUploadSDK/Extensions/FileManager+FileOperations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// FileManager+FileOperations.swift
//

import Foundation

extension FileManager {

// Work around Swift compiler not bridging Dictionary
// and NSDictionary properly when calling attributesOfItem
func fileSizeOfItem(
atPath path: String
) throws -> UInt64 {
(try attributesOfItem(atPath: path) as NSDictionary)
.fileSize()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// NSMutableURLRequest+Reporting.swift
//

import Foundation

extension NSMutableURLRequest {
static func makeJSONPost(
url: URL,
httpBody: Data,
additionalHTTPHeaders: [String: String]
) -> NSMutableURLRequest {
let request = NSMutableURLRequest(
url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0
)

request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
for keypair in additionalHTTPHeaders {
request.setValue(keypair.value, forHTTPHeaderField: keypair.key)
}

request.httpBody = httpBody

return request
}
}
Loading

0 comments on commit fe4407e

Please sign in to comment.