Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - how to upload video in chunks using OkHTTP?

please see my below code which I use to upload a video to a server. However, for large enough videos, I'm getting an OutOfMemory exception.

        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 {
            }
        });

Can someone point me in the right direction on how to avoid OutOfMemory exception? Is there a way I can go from the InputStream to the requestBody?

like image 680
JK140 Avatar asked Jul 15 '17 23:07

JK140


2 Answers

You can create a custom RequestBody which streams the data. You will have to take some care: it may be reused multiple times because OkHttp may decide to retry the request. Make sure you can re-open the InputStream from the start each time.

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();
    }
});
like image 115
ephemient Avatar answered Oct 24 '22 00:10

ephemient


  1. Tries to convert Uri to InputStream using one of AssetFileDescriptor/ ContentResolver/ ParcelFileDescriptor
  2. Creates okio.Source from InputStream (about source/sink)
  3. Write Source into Sink, then close InputStream

If you know what server expects as contentType, hardcode it. Because server might only accept application/octet-stream even if file is video/mp4. Uri to contentType

Check for Uri to contentLength, if contentLength is not found, Content-Length: X header will not be appended when uploading.

/** 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()
                    }
                }
            }
        }
    }
}

Usage:

val request = uri.asRequestBody(
       contentResolver = context.contentResolver,
       contentType = "application/octet-stream".toMediaTypeOrNull(),
       contentLength = uri.length(context.contentResolver)
)
like image 33
Jemshit Iskenderov Avatar answered Oct 24 '22 02:10

Jemshit Iskenderov