0

我目前正在研究一种将视频短视频(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 并将其发送到我的帖子请求正文中是否可行,或者我应该使用其他方式将我的视频上传到服务器中吗?我愿意接受任何建议,谢谢

4

2 回答 2

3
  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

于 2021-12-29T22:33:37.887 回答
0

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

资产已经如此之大,以至于:

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

  • 无论如何,您可能希望避免在任何给定时间将整个资产加载到内存中(即避免Data在构建此上传请求的过程中使用 a )。标准的 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(其中一些似乎是图像选择器本身缓存的内存)。我重复了两次,以说明每次后续上传的内存都不会继续增长。

在此处输入图像描述

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

于 2022-01-01T22:14:54.187 回答