如何在 swift 中将本地视频转换为 base64?

How to convert a local Video to base64 in swift?

我目前正在研究一种将视频短片(10-30 秒)上传到我的数据库的方法,并且正在询问是否可以将视频从本地图库转换为 base64,目前我得到了您可以在这段代码中看到使用 imagePickerController 的视频:

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

我也想知道是否可以将视频保存到 base64 并将其发送到我的 post 请求的正文中,或者我应该使用其他方式将我的视频上传到服务器吗? 我愿意接受任何建议,谢谢

  1. 从文件中获取数据url
  2. 从数据中获取 Base64 字符串
guard let url = info[.mediaURL] as? URL else { return }
let data = Data(contentsOf: url)
let base64String = data.base64EncodedString()

上传文件到服务器使用Multipart/form-data,因为Base64有原始文件大小的4/3

我建议不要对视频进行 base64 编码。

资产已经很大以至于:

  • 您想防止 base64 使资产变得更大(因此上传速度更慢);和

  • 无论如何,您可能希望避免在任何给定时间将整个资产加载到内存中(即避免在构建此上传请求的过程中使用 Data)。标准的 base-64 编码 Data 方法有效地要求您将整个资产存储在内存中以执行 base-64 编码,同时您还将在内存中存储 base-64 字符串。

    例如,对 50 mb 的视频使用标准的 base-64 编码 Data 方法可能会使内存增加至少 116 mb。

multipart/form-data 请求是标准方法(允许嵌入二进制有效负载和发送附加字段)。不过要小心,因为你会在网上找到的大多数例子都会构建一个 Data 然后发送,这可能是不谨慎的。将其写入文件,而无需在任何给定时间尝试将整个资产加载到 RAM 中。然后执行基于文件的上传任务,将其发送到您的服务器。

例如,如果您想自己创建这个多部分请求,您可以执行如下操作:

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

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
        }
    }
}

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
                }
            }
        }
    }
}

然后你可以做这样的事情(在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)
    }
}

或者,在早期的 Swift 版本中:

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

在这里,虽然我上传了两个 55mb 的视频,但总分配量从未超过 8mb(其中一些似乎是由图像选择器本身缓存的内存)。我重复了两次以说明内存不会随着每次后续上传而继续增长。

(绿色区间是image/video picker 花费的时间和相关的视频压缩。红色区间是实际上传的时间。这样你就可以将过程与内存相关联用法。)