iOS: Why my big files are not converted with NSData(contentsOfFile: options:)? Error Domain=NSCocaErrorDomain Code=256

假如想象 提交于 2019-12-06 16:08:06

When dealing with assets as large as that, you want to avoid using Data (and NSData) entirely. So:

  • read the video using an InputStream;
  • write the body of the request to another file using OutputStream; and
  • upload that payload as a file rather than setting the httpBody of the request; and
  • make sure to clean up afterwards, removing that temporary payload file.

All of this avoids ever loading the whole asset into memory at one time and your peak memory usage will be far lower than it would have been if you use Data. This also ensures that this is unlikely to ever fail due to a lack of RAM.

func uploadVideo(_ videoPath: String, fileName: String, eventId: Int, contactId: Int, type: Int, callback: @escaping (_ data: Data?, _ resp: HTTPURLResponse?, _ error: Error?) -> Void) {
    let videoFileURL = URL(fileURLWithPath: videoPath)
    let boundary = generateBoundaryString()

    // build the request

    let request = buildRequest(boundary: boundary)

    // build the payload

    let payloadFileURL: URL

    do {
        payloadFileURL = try buildPayloadFile(videoFileURL: videoFileURL, boundary: boundary, fileName: fileName, eventId: eventId, contactId: contactId, type: type)
    } catch {
        callback(nil, nil, error)
        return
    }

    // perform the upload

    performUpload(request, payload: payloadFileURL, callback: callback)
}

enum UploadError: Error {
    case unableToOpenPayload(URL)
    case unableToOpenVideo(URL)
}

private func buildPayloadFile(videoFileURL: URL, boundary: String, fileName: String, eventId: Int, contactId: Int, type: Int) throws -> URL {
    let mimetype = "video/mp4"

    let payloadFileURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(UUID().uuidString)

    guard let stream = OutputStream(url: payloadFileURL, append: false) else {
        throw UploadError.unableToOpenPayload(payloadFileURL)
    }

    stream.open()

    //define the data post parameter
    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"eventId\"\r\n\r\n")
    stream.write("\(eventId)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"contactId\"\r\n\r\n")
    stream.write("\(contactId)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"type\"\r\n\r\n")
    stream.write("\(type)\r\n")

    stream.write("--\(boundary)\r\n")
    stream.write("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
    stream.write("Content-Type: \(mimetype)\r\n\r\n")
    if stream.append(contentsOf: videoFileURL) < 0 {
        throw UploadError.unableToOpenVideo(videoFileURL)
    }
    stream.write("\r\n")

    stream.write("--\(boundary)--\r\n")
    stream.close()

    return payloadFileURL
}

private func buildRequest(boundary: String) -> URLRequest {
    let WSURL = "https://" + "renauldsqffssfd3.sqdfs.fr/qsdf"

    let requestURLString = "\(WSURL)/qsdfqsf/qsdf/sdfqs/dqsfsdf/"
    let url = URL(string: requestURLString)!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"

    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")

    return request
}

private func performUpload(_ request: URLRequest, payload: URL, callback: @escaping (_ data: Data?, _ resp: HTTPURLResponse?, _ error: Error?) -> Void) {
    let session = URLSession(configuration: .default, delegate: self, delegateQueue: .main)

    let task = session.uploadTask(with: request, fromFile: payload) { data, response, error in
        try? FileManager.default.removeItem(at: payload) // clean up after yourself

        if let response = response as? HTTPURLResponse {
            let status = response.statusCode
        }

        callback(data, response as? HTTPURLResponse, error)
    }

    task.resume()
}

By the way, uploading this as a file also has the virtue that you can consider using a background URLSessionConfiguration at some future date (i.e. the upload of a 4 gb video is likely to take so long that the user might not be inclined to leave the app running and let the upload finish; background sessions let the upload finish even if your app is no longer running; but background uploads require file-based tasks, not relying on the httpBody of the request).

That's a whole different issue, beyond the scope here, but hopefully the above illustrates the key issue here, namely don't use NSData/Data when dealing with assets that are this large.


Please note, the above uses the following extension to OutputStream, including method to write strings to output streams and to append the contents of another file to the stream:

extension OutputStream {
    @discardableResult
    func write(_ string: String) -> Int {
        guard let data = string.data(using: .utf8) else { return -1 }
        return data.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) -> Int in
            write(buffer, maxLength: data.count)
        }
    }

    @discardableResult
    func append(contentsOf url: URL) -> Int {
        guard let inputStream = InputStream(url: url) else { return -1 }
        inputStream.open()
        let bufferSize = 1_024 * 1_024
        var buffer = [UInt8](repeating: 0, count: bufferSize)
        var bytes = 0
        var totalBytes = 0
        repeat {
            bytes = inputStream.read(&buffer, maxLength: bufferSize)
            if bytes > 0 {
                write(buffer, maxLength: bytes)
                totalBytes += bytes
            }
        } while bytes > 0

        inputStream.close()

        return bytes < 0 ? bytes : totalBytes
    }
}

According to Apple documentation, you can use NSData(contentsOf:options:) to "read short files synchronously", so it's not supposed to be able to handle a 4 GB file. Instead you could use InputStream and initialize it with the URL with your file path.

In the catch area you have a error object, this is your answer.

UPD: I supposed this error, and right cause is Code=12 "Cannot allocate memory"

You can try to split like - Is calling read:maxLength: once for every NSStreamEventHasBytesAvailable correct?

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!