skip to Main Content

I am currently working on a method to upload videos short videos (10-30sec) to my data base, and was questioning if is possible to convert a video from the local gallery to base64, at the moment I get the video using the imagePickerController as you can see in this code:

func imagePickerController(_ picker: UIImagePickerController,
                           didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //Here is save the video URL
        let url = info[.mediaURL] as? URL

        //Here goes the code to convert this video URL to base64...
    
        self.dismiss(animated: true)
}

I was also questioning if is viable to save the video to base64 and send it in the body of my post request or should I use other way to upload my video in the server?
I am open to any recommendation, thanks

2

Answers


    1. Get data from file url
    2. Get Base64 string from data
    guard let url = info[.mediaURL] as? URL else { return }
    let data = Data(contentsOf: url)
    let base64String = data.base64EncodedString()
    

    For upload file to server use Multipart/form-data, because Base64 has 4/3 of original file size

    Login or Signup to reply.
  1. I would advise against base64 encoding a video.

    The asset is already so large that:

    • You want to prevent base64 from making the asset even larger (and, therefore, the upload even slower); and

    • You probably want to avoid ever loading the whole asset into memory at any given time, anyway (i.e. avoid using a Data in the process of building this upload request). The standard base-64 encoding Data methods effectively require that you have the entire asset in memory to perform the base-64 encoding, and you will also have the base-64 string in memory at the same time, too.

      E.g., using the standard base-64 encoding Data method for a 50 mb video will probably spike memory up to 116 mb, at least.

    A multipart/form-data request is the standard approach (allows embedding of binary payload and sending of additional fields). Be careful, though, as most examples that you’ll find online build a Data which it then sends, which probably is not prudent. Write it to a file without ever trying to load the whole asset in RAM at any given time. Then perform a file-based upload task to send this to your server.

    For example if you wanted to create this multipart request yourself, you could do something like the following:

    // MARK: - Public interface
    
    extension URLSession {
        /// Delegate-based upload task
    
        @discardableResult
        func uploadTask(
            from url: URL,
            headers: [String: String]? = nil,
            parameters: [String: String]? = nil,
            filePathKey: String,
            fileURLs: [URL]
        ) throws -> URLSessionUploadTask {
            let (request, fileURL) = try uploadRequestFile(from: url, headers: headers, parameters: parameters, filePathKey: filePathKey, fileURLs: fileURLs)
            return uploadTask(with: request, fromFile: fileURL)
        }
    
        /// Completion-handler-based upload task
    
        @discardableResult
        func uploadTask(
            from url: URL,
            headers: [String: String]? = nil,
            parameters: [String: String]? = nil,
            filePathKey: String,
            fileURLs: [URL],
            completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
        ) -> URLSessionUploadTask? {
            do {
                let (request, fileURL) = try uploadRequestFile(
                    from: url,
                    headers: headers,
                    parameters: parameters,
                    filePathKey: filePathKey,
                    fileURLs: fileURLs
                )
                return uploadTask(with: request, fromFile: fileURL, completionHandler: completionHandler)
            } catch {
                completionHandler(nil, nil, error)
                return nil
            }
        }
    
        /// Async-await-based upload task
    
        @available(iOS 15.0, *)
        func upload(
            from url: URL,
            headers: [String: String]? = nil,
            parameters: [String: String]? = nil,
            filePathKey: String,
            fileURLs: [URL],
            delegate: URLSessionTaskDelegate? = nil
        ) async throws -> (Data, URLResponse) {
            let (request, fileURL) = try uploadRequestFile(
                from: url,
                headers: headers,
                parameters: parameters,
                filePathKey: filePathKey,
                fileURLs: fileURLs
            )
            return try await upload(for: request, fromFile: fileURL, delegate: delegate)
        }
    }
    
    // MARK: - Private implementation
    
    private extension URLSession {
        private func uploadRequestFile(
            from url: URL,
            headers: [String: String]? = nil,
            parameters: [String: String]? = nil,
            filePathKey: String,
            fileURLs: [URL]
        ) throws -> (URLRequest, URL) {
            let boundary = "Boundary-" + UUID().uuidString
    
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("multipart/form-data; boundary=(boundary)", forHTTPHeaderField: "Content-Type")
    
            headers?.forEach { (key, value) in
                request.addValue(value, forHTTPHeaderField: key)
            }
    
            let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
                .appendingPathComponent(UUID().uuidString)
    
            guard let stream = OutputStream(url: fileURL, append: false) else {
                throw OutputStreamError.unableToCreateFile
            }
    
            stream.open()
            
            try parameters?.forEach { (key, value) in
                try stream.write("--(boundary)rn")
                try stream.write("Content-Disposition: form-data; name="(key)"rnrn")
                try stream.write("(value)rn")
            }
    
            for fileURL in fileURLs {
                let filename = fileURL.lastPathComponent
    
                try stream.write("--(boundary)rn")
                try stream.write("Content-Disposition: form-data; name="(filePathKey)"; filename="(filename)"rn")
                try stream.write("Content-Type: (fileURL.mimeType)rnrn")
                try stream.write(from: fileURL)
                try stream.write("rn")
            }
    
            try stream.write("--(boundary)--rn")
    
            stream.close()
    
            return (request, fileURL)
        }
    }
    

    and

    extension URL {
        /// Mime type for the URL
        ///
        /// Requires `import UniformTypeIdentifiers` for iOS 14 solution.
        /// Requires `import MobileCoreServices` for pre-iOS 14 solution
    
        var mimeType: String {
            if #available(iOS 14.0, *) {
                return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
            } else {
                guard
                    let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
                    let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() as String?
                else {
                    return "application/octet-stream"
                }
    
                return mimeType
            }
        }
    }
    

    and

    enum OutputStreamError: Error {
        case stringConversionFailure
        case bufferFailure
        case writeFailure
        case unableToCreateFile
        case unableToReadFile
    }
    
    extension OutputStream {
    
        /// Write `String` to `OutputStream`
        ///
        /// - parameter string:                The `String` to write.
        /// - parameter encoding:              The `String.Encoding` to use when writing the string. This will default to `.utf8`.
        /// - parameter allowLossyConversion:  Whether to permit lossy conversion when writing the string. Defaults to `false`.
    
        func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
            guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
                throw OutputStreamError.stringConversionFailure
            }
            try write(data)
        }
    
        /// Write `Data` to `OutputStream`
        ///
        /// - parameter data:                  The `Data` to write.
    
        func write(_ data: Data) throws {
            try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
                guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                    throw OutputStreamError.bufferFailure
                }
    
                var bytesRemaining = buffer.count
    
                while bytesRemaining > 0 {
                    let bytesWritten = write(pointer, maxLength: bytesRemaining)
                    if bytesWritten < 0 {
                        throw OutputStreamError.writeFailure
                    }
    
                    bytesRemaining -= bytesWritten
                    pointer += bytesWritten
                }
            }
        }
    
        /// Write `Data` to `OutputStream`
        ///
        /// - parameter data:                  The `Data` to write.
    
        func write(from url: URL) throws {
            guard let input = InputStream(url: url) else {
                throw OutputStreamError.unableToReadFile
            }
    
            input.open()
            defer { input.close() }
    
            let bufferSize = 65_536
    
            var data = Data(repeating: 0, count: bufferSize)
    
            try data.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) throws in
                guard let buffer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                    throw OutputStreamError.bufferFailure
                }
    
                while input.hasBytesAvailable {
                    var remainingCount = input.read(buffer, maxLength: bufferSize)
                    if remainingCount < 0 { throw OutputStreamError.unableToReadFile }
    
                    var pointer = buffer
                    while remainingCount > 0 {
                        let countWritten = write(pointer, maxLength: remainingCount)
                        if countWritten < 0 { throw OutputStreamError.writeFailure }
                        remainingCount -= countWritten
                        pointer += countWritten
                    }
                }
            }
        }
    }
    

    Then you can do things like (in iOS 15):

    extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            dismiss(animated: true)
        }
    
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            guard let fileURL = info[.mediaURL] as? URL else {
                print("no media URL")
                return
            }
    
            Task {
                do {
                    let (data, response) = try await URLSession.shared.upload(from: url, filePathKey: "file", fileURLs: [fileURL])
                    try? FileManager.default.removeItem(at: fileURL)
    
                    // check `data` and `response` here
                } catch {
                    print(error)
                }
            }
    
            dismiss(animated: true)
        }
    }
    

    Or, in earlier Swift versions:

    extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            dismiss(animated: true)
        }
    
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            guard let fileURL = info[.mediaURL] as? URL else {
                print("no media URL")
                return
            }
    
            URLSession.shared.uploadTask(from: url, filePathKey: "file", fileURLs: [fileURL]) { data, _, error in
                try? FileManager.default.removeItem(at: fileURL)
    
                guard let data = data, error == nil else {
                    print(error!)
                    return
                }
    
                // check `data` and `response` here
            }?.resume()
    
            dismiss(animated: true)
        }
    }
    

    Here, although I uploaded two 55mb videos, total allocations never exceeded 8mb (and some of that appears to be memory cached by the image picker, itself). I repeated it twice to illustrate that the memory does not continue to grow for each subsequent upload.

    enter image description here

    (The green intervals are the time spent in the image/video picker and the associated compression of the video. The red interval is the time of the actual upload. This way you can correlate the process with the memory usage.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search