Android - 如何使用OkHTTP分块上传视频?

7
请看下面的代码,这是我用来上传视频到服务器的。然而,对于足够大的视频,我会遇到OutOfMemory异常。
        InputStream stream = getContentResolver().openInputStream(videoUri);
        byte[] byteArray = IOUtils.toByteArray(stream);
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("file", "fname",
                        RequestBody.create(MediaType.parse("video/mp4"), byteArray))
                .build();
        Request request = new Request.Builder()
                .url(uploadURL)
                .post(requestBody)
                .build();

        OkHttpClient client = new OkHttpClient.Builder().build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });

请问有没有人可以指导一下如何避免OutOfMemory异常?我能否从InputStream转化为requestBody?

2个回答

8

您可以创建自定义的RequestBody来流式传输数据。您需要注意一些事项:它可能会被多次重用,因为OkHttp可能会决定重试请求。请确保每次都能从开头重新打开InputStream

ContentResolver contentResolver = context.getContentResolver();
final String contentType = contentResolver.getType(videoUri);
final AssetFileDescriptor fd = contentResolver.openAssetFileDescriptor(videoUri, "r");
if (fd == null) {
    throw new FileNotFoundException("could not open file descriptor");
}
RequestBody videoFile = new RequestBody() {
    @Override public long contentLength() { return fd.getDeclaredLength(); }
    @Override public MediaType contentType() { return MediaType.parse(contentType); }
    @Override public void writeTo(BufferedSink sink) throws IOException {
        try (InputStream is = fd.createInputStream()) {
            sink.writeAll(Okio.buffer(Okio.source(is)));
        }
    }
};
RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("file", "fname", videoFile)
        .build();
Request request = new Request.Builder()
        .url(uploadURL)
        .post(requestBody)
        .build();
client.newCall(request).enqueue(new Callback() {
    @Override public void onFailure(Call call, IOException e) {
        try {
            fd.close();
        } catch (IOException ex) {
            e.addSuppressed(ex);
        }
        Log.e(TAG, "failed", e);
    }
    @Override public void onResponse(Call call, Response response) throws IOException {
        fd.close();
    }
});

非常感谢!您知道我如何在请求重试的情况下重新打开流吗? - JK140
此外,为什么只有在收到响应后才调用fd.close()函数? - JK140
1
@JK140 1. 每次调用 RequestBody.writeTo() 时,都打开一个 InputStream,就像这个例子所做的那样。如果你在 writeTo() 外面打开 InputStream,它将在第一次读取整个数据后不再具备数据。2. AssetFileDescriptor 在响应完成之前会一直被使用(必须关闭,否则会导致资源泄漏)。你也可以 使用 AssetFileDescriptor,而是在 writeTo 中获取长度(可能使用 ContentResolver.query()),并使用和关闭 ContentResolver.openInputStream() - ephemient
真的是一个很好的答案。对于那些不使用传输编码:分块来传输数据的人,这里有一个注脚:你只需要覆盖contentLength()方法,OKHttp就会自动添加Content-Length头到请求中。fd.getDeclaredLength()将返回-1,所以传输编码仍然是“chuked”;而fd.getLength()将返回文件的实际大小,因此chunked被禁用了。 - vainquit

4
  1. 使用AssetFileDescriptor/ ContentResolver/ ParcelFileDescriptor之一将Uri转换为InputStream
  2. InputStream创建okio.Source(有关源/汇的详细信息请参见source/sink
  3. Source写入Sink,然后关闭InputStream

如果您知道服务器所期望的contentType,请硬编码。因为即使文件是video/mp4,服务器可能只接受application/octet-streamUri to contentType

检查Uri to contentLength,如果未找到contentLength,则上传时不会追加Content-Length: X头。

/** It supports file/content/mediaStore/asset URIs. asset not tested */
fun createAssetFileDescriptor() = try {
    contentResolver.openAssetFileDescriptor(this, "r")
} catch (e: FileNotFoundException) {
    null
}

/** It supports file/content/mediaStore URIs. Will not work with providers that return sub-sections of files */
fun createParcelFileDescriptor() = try {
    contentResolver.openFileDescriptor(this, "r")
} catch (e: FileNotFoundException) {
    null
}

/** - It supports file/content/mediaStore/asset URIs. asset not tested
 * - When file URI is used, may get contentLength error (expected x but got y) error when uploading if contentLength header is filled from assetFileDescriptor.length */
fun createInputStreamFromContentResolver() = try {
    contentResolver.openInputStream(this)
} catch (e: FileNotFoundException) {
    null
}

fun Uri.asRequestBody(contentResolver: ContentResolver,
                      contentType: MediaType? = null,
                      contentLength: Long = -1L)
        : RequestBody {

    return object : RequestBody() {
        /** If null is given, it is binary for Streams */
        override fun contentType() = contentType

        /** 'chunked' transfer encoding will be used for big files when length not specified */
        override fun contentLength() = contentLength

        /** This may get called twice if HttpLoggingInterceptor is used */
        override fun writeTo(sink: BufferedSink) {
            val assetFileDescriptor = createAssetFileDescriptor()
            if (assetFileDescriptor != null) {
                // when InputStream is closed, it auto closes AssetFileDescriptor
                AssetFileDescriptor.AutoCloseInputStream(assetFileDescriptor)
                        .source()
                        .use { source -> sink.writeAll(source) }
            } else {
                val inputStream = createInputStreamFromContentResolver()
                if (inputStream != null) {
                    inputStream
                            .source()
                            .use { source -> sink.writeAll(source) }
                } else {
                    val parcelFileDescriptor = createParcelFileDescriptor()
                    if (parcelFileDescriptor != null) {
                        // when InputStream is closed, it auto closes ParcelFileDescriptor
                        ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor)
                                .source()
                                .use { source -> sink.writeAll(source) }
                    } else {
                        throw IOException()
                    }
                }
            }
        }
    }
}

使用方法:

val request = uri.asRequestBody(
       contentResolver = context.contentResolver,
       contentType = "application/octet-stream".toMediaTypeOrNull(),
       contentLength = uri.length(context.contentResolver)
)

1
如果您将本地函数从“fun”主体移动到匿名类中,则可以节省一些字节码。 - Miha_x64

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接