| 1 | import UIKit |
| 2 | import PhotosUI |
| 3 | import fp_swift_upload_sdk |
| 4 | import Foundation |
| 5 | import Network |
| 6 | |
| 7 | class SelectUploadVideoViewController: UIViewController, PHPickerViewControllerDelegate, UITextFieldDelegate, UploadProgressDelegate, UploadSDKErrorDelegate, UIDocumentPickerDelegate { |
| 8 | |
| 9 | |
| 10 | @IBOutlet weak var thumbNailImage: UIImageView! |
| 11 | @IBOutlet weak var button_upload: UIButton! |
| 12 | @IBOutlet weak var progressBar: UIProgressView! |
| 13 | @IBOutlet weak var label_progress: UILabel! |
| 14 | @IBOutlet weak var button_selectVideo: UIButton! |
| 15 | @IBOutlet weak var errorLabel: UILabel! |
| 16 | @IBOutlet weak var buttonsStack: UIStackView! |
| 17 | @IBOutlet weak var button_pause: UIButton! |
| 18 | @IBOutlet weak var chunkSize: UITextField! |
| 19 | @IBOutlet weak var uploadedChunk: UILabel! |
| 20 | @IBOutlet var button_abort: UIButton! |
| 21 | |
| 22 | var isOfflineModeEnabled = false |
| 23 | var uploadIsPaused = false |
| 24 | var file : URL? |
| 25 | var pauseBtnClicked = false |
| 26 | var uploader = Uploads() |
| 27 | var picje = VideoPickerConfiguration() |
| 28 | var thumGenerater = ThumbnailImageGenerater() |
| 29 | var authenticatedURL: URL? = nil |
| 30 | var videoInputURL: URL? = nil |
| 31 | |
| 32 | let myServerBackend = UploadURLGenerater(urlSession: URLSession(configuration: URLSessionConfiguration.default)) |
| 33 | private var prepareTask: Task<Void, Never>? = nil |
| 34 | public var isPaused = false |
| 35 | private var assetRequestId: PHImageRequestID? = nil |
| 36 | |
| 37 | let session = URLSession.shared |
| 38 | var requestData = [String: Any]() |
| 39 | var chunks : Int = 0 |
| 40 | var resData = [String]() |
| 41 | var urlIndex = 0 |
| 42 | var chunksList: [String] = [] |
| 43 | var chunksArray: [Data] = [] |
| 44 | var signedURL: URL? |
| 45 | var objectName: String? |
| 46 | var uploadId: String? |
| 47 | let monitor = NWPathMonitor() |
| 48 | var isInternetAvailable = false |
| 49 | var chunkAttempt = 0 |
| 50 | |
| 51 | override func viewDidLoad() { |
| 52 | super.viewDidLoad() |
| 53 | chunkSize.delegate = self |
| 54 | uploader.progressDelegate = self |
| 55 | uploader.errorDelegate = self |
| 56 | button_abort.isHidden = true |
| 57 | pickerConfig() |
| 58 | title = "Create New Upload" |
| 59 | button_upload.isHidden = false |
| 60 | button_pause.isHidden = true |
| 61 | PHPhotoLibrary.authorizationStatus(for: .readWrite) |
| 62 | button_upload.isEnabled = false |
| 63 | button_upload.backgroundColor = .lightGray |
| 64 | |
| 65 | // Set the progress handler |
| 66 | progressBar.isHidden = true |
| 67 | label_progress.isHidden = true |
| 68 | uploader.progressHandler = { [weak self] progress in |
| 69 | |
| 70 | // Update UI with the progress value |
| 71 | DispatchQueue.main.async { |
| 72 | self?.progressBar.layer.cornerRadius = 10 |
| 73 | self?.progressBar.isHidden = false |
| 74 | self?.label_progress.isHidden = false |
| 75 | |
| 76 | // Update progress bar or any other UI element |
| 77 | self?.progressBar.progress = progress |
| 78 | self?.label_progress.text = String(format: "%.0f%%", progress * 100) |
| 79 | if progress == 1.0 { |
| 80 | self?.button_pause.isHidden = true |
| 81 | self?.button_abort.isHidden = true |
| 82 | self?.button_upload.isHidden = false |
| 83 | let alertController = UIAlertController(title: "success", message: "The video file uploaded successfully.", preferredStyle: .alert) |
| 84 | let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) |
| 85 | alertController.addAction(okAction) |
| 86 | self?.present(alertController, animated: true, completion: nil) |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | startMonitoring() |
| 92 | |
| 93 | NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) |
| 94 | |
| 95 | chunkSize.attributedPlaceholder = NSAttributedString( |
| 96 | string: "Enter chunk size", |
| 97 | attributes: [ |
| 98 | .foregroundColor: UIColor.white // Change to your desired color |
| 99 | ] |
| 100 | ) |
| 101 | } |
| 102 | |
| 103 | func didUpdateProgressText(_ text: String) { |
| 104 | DispatchQueue.main.async { |
| 105 | self.uploadedChunk.text = text |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func uploadSDKDidFail(with error: String) { |
| 110 | DispatchQueue.main.async { |
| 111 | self.displayErrorAlert(error: error) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
| 116 | textField.resignFirstResponder() // Dismisses the keyboard |
| 117 | return true |
| 118 | } |
| 119 | |
| 120 | func startMonitoring() { |
| 121 | monitor.pathUpdateHandler = { path in |
| 122 | DispatchQueue.main.async { [self] in |
| 123 | self.isInternetAvailable = path.status == .satisfied |
| 124 | if self.isInternetAvailable { |
| 125 | if uploadIsPaused { |
| 126 | if self.chunkAttempt < chunksArray.count { |
| 127 | |
| 128 | } |
| 129 | } |
| 130 | } else { |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | let queue = DispatchQueue(label: "NetworkMonitor") |
| 135 | monitor.start(queue: queue) |
| 136 | } |
| 137 | |
| 138 | @objc func appDidBecomeActive() { |
| 139 | startMonitoring() |
| 140 | } |
| 141 | |
| 142 | func pickerConfig() { |
| 143 | let alert = UIAlertController(title: "Select Media Type", message: nil, preferredStyle: .actionSheet) |
| 144 | |
| 145 | // Option to pick Video |
| 146 | alert.addAction(UIAlertAction(title: "Pick Video", style: .default, handler: { _ in |
| 147 | let config = self.picje.pickerConfig() // video-only config |
| 148 | let picker = PHPickerViewController(configuration: config) |
| 149 | picker.delegate = self |
| 150 | picker.modalPresentationStyle = .overCurrentContext |
| 151 | self.present(picker, animated: true) |
| 152 | })) |
| 153 | |
| 154 | // Option to pick Audio |
| 155 | alert.addAction(UIAlertAction(title: "Pick Audio", style: .default, handler: { _ in |
| 156 | let audioPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.audio]) |
| 157 | audioPicker.allowsMultipleSelection = false |
| 158 | audioPicker.delegate = self |
| 159 | self.present(audioPicker, animated: true) |
| 160 | })) |
| 161 | |
| 162 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) |
| 163 | |
| 164 | self.present(alert, animated: true) |
| 165 | } |
| 166 | |
| 167 | @IBAction func button_pauseVideo(_ sender: UIButton) { |
| 168 | sender.isEnabled = false |
| 169 | if sender.isSelected { |
| 170 | sender.setTitle("Pause", for: .normal) |
| 171 | uploadIsPaused = false |
| 172 | pauseBtnClicked = true |
| 173 | uploader.resume() |
| 174 | sender.backgroundColor = .systemGreen |
| 175 | |
| 176 | } else { |
| 177 | sender.setTitle("Resume", for: .normal) |
| 178 | uploadIsPaused = true |
| 179 | pauseBtnClicked = false |
| 180 | startMonitoring() |
| 181 | uploader.pause() |
| 182 | sender.backgroundColor = .systemRed |
| 183 | } |
| 184 | |
| 185 | sender.isSelected.toggle() |
| 186 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { |
| 187 | sender.isEnabled = true |
| 188 | |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | @IBAction func button_uploadVideo(_ sender: UIButton) { |
| 193 | button_upload.isHidden = true |
| 194 | button_pause.isHidden = false |
| 195 | button_abort.isHidden = false |
| 196 | self.progressBar.isHidden = false |
| 197 | self.label_progress.isHidden = false |
| 198 | self.label_progress.text = String(format: "%.0f%%", 0.0000000 * 100) |
| 199 | |
| 200 | Task { |
| 201 | let createUploadURL = try await self.myServerBackend.createDirectUpload() |
| 202 | uploader.uploadFile(file: file!, endpoint: createUploadURL.absoluteString, chunkSizeKB: Int(chunkSize.text ?? "") ?? 0) |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | @IBAction func button_abort(_ sender: UIButton) { |
| 207 | button_pause.isHidden = true |
| 208 | button_abort.isHidden = true |
| 209 | button_upload.isHidden = false |
| 210 | uploader.abort() |
| 211 | sender.backgroundColor = .red |
| 212 | } |
| 213 | |
| 214 | @IBAction func button_selectVideo(_ sender: UIButton) { |
| 215 | pickerConfig() |
| 216 | } |
| 217 | |
| 218 | func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { |
| 219 | dismiss(animated: true, completion: nil) |
| 220 | guard let result = results.first else { |
| 221 | return |
| 222 | } |
| 223 | |
| 224 | if let assetRequestId = self.assetRequestId { |
| 225 | PHImageManager.default().cancelImageRequest(assetRequestId) |
| 226 | } |
| 227 | let tempDir = FileManager.default.temporaryDirectory |
| 228 | let tempFile = URL(string: "upload-\(Date().timeIntervalSince1970).mp4", relativeTo: tempDir)! |
| 229 | |
| 230 | guard let assetIdentitfier = result.assetIdentifier else { |
| 231 | print("No Asset ID for chosen asset") |
| 232 | return |
| 233 | } |
| 234 | |
| 235 | guard let assetIdentitfier = result.assetIdentifier else { |
| 236 | print("No Asset ID for chosen asset") |
| 237 | return |
| 238 | |
| 239 | } |
| 240 | |
| 241 | let options = PHFetchOptions() |
| 242 | options.includeAssetSourceTypes = [.typeUserLibrary, .typeCloudShared] |
| 243 | let phAssetResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentitfier], options: options) |
| 244 | guard let phAsset = phAssetResult.firstObject else { |
| 245 | return |
| 246 | } |
| 247 | |
| 248 | let exportOptions = PHVideoRequestOptions() |
| 249 | exportOptions.isNetworkAccessAllowed = true |
| 250 | exportOptions.deliveryMode = .highQualityFormat |
| 251 | assetRequestId = PHImageManager.default().requestExportSession(forVideo: phAsset, options: exportOptions, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: {(exportSession, info) -> Void in |
| 252 | DispatchQueue.main.async { [weak self] in |
| 253 | guard let exportSession = exportSession else { |
| 254 | return |
| 255 | } |
| 256 | self?.thumGenerater.extractThumbnailAsync(exportSession.asset) { thumbnailImage in |
| 257 | guard let thumbImage = thumbnailImage else { return } |
| 258 | DispatchQueue.main.async { |
| 259 | self?.thumbNailImage.image = UIImage(cgImage: thumbImage) |
| 260 | self?.thumbNailImage.isHidden = false |
| 261 | self?.button_selectVideo.isUserInteractionEnabled = false |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | }) |
| 266 | |
| 267 | result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier , completionHandler: { [ weak self] url, error in |
| 268 | guard let fileURL = url else { |
| 269 | return |
| 270 | } |
| 271 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first |
| 272 | guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return } |
| 273 | self?.file = (documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent))! |
| 274 | |
| 275 | do { |
| 276 | if FileManager.default.fileExists(atPath: targetURL.path) { |
| 277 | try FileManager.default.removeItem(at: targetURL) |
| 278 | } else { |
| 279 | // print("file path not exists") |
| 280 | |
| 281 | } |
| 282 | |
| 283 | try FileManager.default.copyItem(at: fileURL, to: targetURL) |
| 284 | self?.videoInputURL = targetURL |
| 285 | DispatchQueue.main.async { |
| 286 | self?.button_upload.isEnabled = true |
| 287 | self?.button_upload.backgroundColor = .green |
| 288 | } |
| 289 | } catch { |
| 290 | self?.displayErrorAlert(error: error.localizedDescription) |
| 291 | } |
| 292 | }) |
| 293 | } |
| 294 | |
| 295 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { |
| 296 | guard let fileURL = urls.first else { return } |
| 297 | |
| 298 | // Start accessing security-scoped resource |
| 299 | let didStartAccessing = fileURL.startAccessingSecurityScopedResource() |
| 300 | defer { |
| 301 | if didStartAccessing { |
| 302 | fileURL.stopAccessingSecurityScopedResource() |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | // Confirm access granted |
| 307 | guard didStartAccessing else { |
| 308 | print("Failed to access security-scoped resource") |
| 309 | self.displayErrorAlert(error: "Unable to access selected file due to security permissions.") |
| 310 | return |
| 311 | } |
| 312 | |
| 313 | // Now safe to use fileURL |
| 314 | print("Picked audio file: \(fileURL.lastPathComponent)") |
| 315 | |
| 316 | // Copy to local directory |
| 317 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first |
| 318 | guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return } |
| 319 | self.file = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) |
| 320 | |
| 321 | do { |
| 322 | if FileManager.default.fileExists(atPath: targetURL.path) { |
| 323 | try FileManager.default.removeItem(at: targetURL) |
| 324 | } |
| 325 | try FileManager.default.copyItem(at: fileURL, to: targetURL) |
| 326 | |
| 327 | self.videoInputURL = targetURL // Or self.audioInputURL |
| 328 | DispatchQueue.main.async { |
| 329 | self.button_upload.isEnabled = true |
| 330 | self.button_upload.backgroundColor = .green |
| 331 | } |
| 332 | } catch { |
| 333 | self.displayErrorAlert(error: error.localizedDescription) |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | func pickerDidCancel(_ picker: PHPickerViewController) { |
| 338 | dismiss(animated: true, completion: nil) |
| 339 | } |
| 340 | |
| 341 | // Display an error message |
| 342 | func displayErrorAlert(error: String) { |
| 343 | let alertController = UIAlertController(title: "Error", message: error, preferredStyle: .alert) |
| 344 | let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in |
| 345 | DispatchQueue.main.async { |
| 346 | self?.thumbNailImage.isHidden = false |
| 347 | self?.button_selectVideo.isUserInteractionEnabled = true |
| 348 | if error == "The current upload was aborted. Please try uploading a new video" { |
| 349 | self?.navigationController?.popViewController(animated: true) |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | alertController.addAction(okAction) |
| 354 | present(alertController, animated: true, completion: nil) |
| 355 | } |
| 356 | |
| 357 | deinit { |
| 358 | monitor.cancel() |
| 359 | NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | class UploadURLGenerater { |
| 364 | |
| 365 | public func presentErrorAlert(on viewController: UIViewController, withTitle title: String, message: String) { |
| 366 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) |
| 367 | let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) |
| 368 | alertController.addAction(okAction) |
| 369 | viewController.present(alertController, animated: true, completion: nil) |
| 370 | } |
| 371 | |
| 372 | func createDirectUpload() async throws -> URL { |
| 373 | |
| 374 | let parameters: [String: Any] = [ |
| 375 | "corsOrigin": "*", |
| 376 | "pushMediaSettings": [ |
| 377 | "accessPolicy": "public", |
| 378 | "generateSubtitles": true, |
| 379 | "normalizeAudio": true, |
| 380 | "maxResolution": "1080p" |
| 381 | ] |
| 382 | ] |
| 383 | |
| 384 | guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else { |
| 385 | throw NSError(domain: "com.example.app", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize parameters"]) |
| 386 | } |
| 387 | |
| 388 | let request = try { |
| 389 | var req = try URLRequest(url:fullURL(forEndpoint: "upload")) |
| 390 | req.httpBody = jsonData |
| 391 | req.httpMethod = "POST" |
| 392 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") |
| 393 | req.addValue("application/json", forHTTPHeaderField: "accept") |
| 394 | |
| 395 | let basicAuthCredential = "\(FASTPIX_ACCESS_TOKEN_ID):\(FASTPIX_SECRET_KEY)".data(using: .utf8)!.base64EncodedString() |
| 396 | req.addValue("Basic \(basicAuthCredential)", forHTTPHeaderField: "Authorization") |
| 397 | return req |
| 398 | }() |
| 399 | |
| 400 | |
| 401 | |
| 402 | let (data, response) = try await urlSession.data(for: request) |
| 403 | let httpResponse = response as! HTTPURLResponse |
| 404 | print("Response",httpResponse.statusCode) |
| 405 | var uploadUrl: URL? |
| 406 | if (200...299).contains(httpResponse.statusCode) { |
| 407 | do { |
| 408 | // Assuming responseData is the Data object you want to convert to a dictionary |
| 409 | if let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { |
| 410 | var dataDic = jsonDictionary["data"] as? NSDictionary |
| 411 | uploadUrl = URL(string: dataDic?.value(forKey: "url") as? String ?? "") |
| 412 | } else { |
| 413 | print("Failed to convert data to dictionary") |
| 414 | } |
| 415 | } catch { |
| 416 | print("Error converting data to dictionary: \(error.localizedDescription)") |
| 417 | } |
| 418 | guard let uploadUrl = uploadUrl else { return URL(string: "")!} |
| 419 | return uploadUrl |
| 420 | } else { |
| 421 | throw CreateUploadError(message: "Upload POST failed: HTTP \(httpResponse.statusCode):\n\(String(decoding: data, as: UTF8.self))") |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | /// Generates a full URL for a given endpoint in the FastPix Video public API |
| 426 | private func fullURL(forEndpoint: String) throws -> URL { |
| 427 | guard let url = URL(string: "https://venus-v1.fastpix.dev/on-demand/\(forEndpoint)") else { |
| 428 | throw CreateUploadError(message: "bad endpoint") |
| 429 | } |
| 430 | return url |
| 431 | } |
| 432 | |
| 433 | // https://dev-api.fastpix.com |
| 434 | let urlSession: URLSession |
| 435 | let jsonEncoder: JSONEncoder |
| 436 | let jsonDecoder: JSONDecoder |
| 437 | |
| 438 | //Gowtham Venus |
| 439 | let FASTPIX_ACCESS_TOKEN_ID = "ACCESS TOKEN" |
| 440 | let FASTPIX_SECRET_KEY = "SECRET KEY" |
| 441 | |
| 442 | init(urlSession: URLSession) { |
| 443 | self.urlSession = urlSession |
| 444 | self.jsonEncoder = JSONEncoder() |
| 445 | self.jsonEncoder.keyEncodingStrategy = JSONEncoder.KeyEncodingStrategy.convertToSnakeCase |
| 446 | self.jsonDecoder = JSONDecoder() |
| 447 | self.jsonDecoder.keyDecodingStrategy = JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase |
| 448 | } |
| 449 | |
| 450 | convenience init() { |
| 451 | self.init(urlSession: URLSession(configuration: URLSessionConfiguration.default)) |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | struct CreateUploadError : Error { |
| 456 | let message: String |
| 457 | } |
| 458 | |
| 459 | fileprivate struct CreateUploadPost: Codable { |
| 460 | var newAssetSettings: NewAssetSettings = NewAssetSettings() |
| 461 | var corsOrigin: String = "*" |
| 462 | } |
| 463 | |
| 464 | fileprivate struct NewAssetSettings: Codable { |
| 465 | var playbackPolicy: [String] = ["public"] |
| 466 | var passthrough: String = "Extra video data. This can be any data and it's for your use" |
| 467 | var mp4Support: String = "standard" |
| 468 | var normalizeAudio: Bool = true |
| 469 | var test: Bool = false |
| 470 | } |
| 471 | |
| 472 | fileprivate struct CreateUploadResponse: Decodable { |
| 473 | var url: String |
| 474 | var id: String |
| 475 | var timeout: Int64 |
| 476 | var status: String |
| 477 | } |
| 478 | |
| 479 | fileprivate struct CreateUploadResponseContainer: Decodable { |
| 480 | var data: CreateUploadResponse |
| 481 | } |