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.

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