如何在 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 请求的正文中,或者我应该使用其他方式将我的视频上传到服务器吗?
我愿意接受任何建议,谢谢
- 从文件中获取数据url
- 从数据中获取 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 花费的时间和相关的视频压缩。红色区间是实际上传的时间。这样你就可以将过程与内存相关联用法。)
我目前正在研究一种将视频短片(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 请求的正文中,或者我应该使用其他方式将我的视频上传到服务器吗? 我愿意接受任何建议,谢谢
- 从文件中获取数据url
- 从数据中获取 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 花费的时间和相关的视频压缩。红色区间是实际上传的时间。这样你就可以将过程与内存相关联用法。)