2

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

Ricardo Guerrero
  • 433
  • 1
  • 5
  • 21

2 Answers2

5

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)\r\n")
            try stream.write("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
            try stream.write("\(value)\r\n")
        }

        for fileURL in fileURLs {
            let filename = fileURL.lastPathComponent

            try stream.write("--\(boundary)\r\n")
            try stream.write("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n")
            try stream.write("Content-Type: \(fileURL.mimeType)\r\n\r\n")
            try stream.write(from: fileURL)
            try stream.write("\r\n")
        }

        try stream.write("--\(boundary)--\r\n")

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • You saved my time and mental health. Thanks a lot. – Subramani Jan 13 '23 at 13:16
  • I’d like to read a video file from my app documents dir to play in a WKwebview (HTML5 – Josiah Feb 25 '23 at 07:24
  • I don’t see how base64 would help. (I wouldn’t be inclined to use a `WKWebView`, either. I use [`AVPlayer`](https://developer.apple.com/documentation/avfoundation/avplayer/) in my projects. See [Media Playback](https://developer.apple.com/documentation/avfoundation/media_playback).) – Rob Feb 25 '23 at 12:33
  • I’m wrapping a web presentation app that displays several layers of overlapping media, text and canvas elements. Using a webview seemed the easiest way to do that, but I want it to be able to work offline, i.e. load/play videos/images from local dir (rather than load from my web server). Maybe there’s not a simple way? – Josiah Feb 26 '23 at 06:12
  • Well, base64-encoding a video (for inlining it, like one might do for an image), is not practical, IMHO. I am surprised you can't store the html and the image asset in the application support directory, or the like. Regardless, it has little to do with this question, here, and as such, this comment area is not the right forum for this discussion. Post your own question. – Rob Feb 26 '23 at 12:48
  • 1
    @Rob you are a life savior. super article. – Sourabh Shekhar Mar 23 '23 at 06:34
4
  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

ILYA2606
  • 587
  • 3
  • 7
  • @Rob 1) Yes, 4/3 of original size 2) CryptoSwift is most popular library for swift crypto algorithms, but you can implement Base64 encoding yourself – ILYA2606 Dec 30 '21 at 08:14
  • Yes, you're right. CryptoSwift has several extensions for Base64, which are not required for a regular tasks. I correct code in answer, thanks! – ILYA2606 Dec 31 '21 at 14:31