如何快速将本地视频转换为base64?

Ric*_*ero 2 base64 ios video-gallery swift urlsession

我目前正在研究一种将视频短视频(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)
}
Run Code Online (Sandbox Code Playgroud)

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

Rob*_*Rob 5

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

\n

该资产已经如此庞大:

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

    \n
  • \n
  • 无论如何,您可能希望避免在任何给定时间将整个资产加载到内存中(即避免Data在构建此上传请求的过程中使用 a )。标准的 Base-64 编码Data方法实际上要求您在内存中拥有整个资产才能执行 Base-64 编码,并且同时内存中也将具有 Base-64 字符串。

    \n

    例如,对Data50 MB 的视频使用标准的 Base-64 编码方法可能会使内存至少达到 116 MB。

    \n
  • \n
\n

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

\n

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

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

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

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

然后你可以执行以下操作(在 iOS 15 中):

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

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

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

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

\n

在此输入图像描述

\n

(绿色间隔是在图像/视频选择器和相关视频压缩中花费的时间。红色间隔是实际上传的时间。这样您就可以将进程与内存使用情况关联起来。)

\n