Skip to content

Commit a66683a

Browse files
committed
ffmpeg: reallocate output buffer dynamically
With FFmpeg we can't determine size of output buffer ahead of time for all codecs, so we need to reallocate it when needed instead of simply failing.
1 parent 88f554c commit a66683a

File tree

4 files changed

+90
-19
lines changed

4 files changed

+90
-19
lines changed

libraries/decoder/src/main/java/androidx/media3/decoder/SimpleDecoderOutputBuffer.java

+20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package androidx.media3.decoder;
1717

1818
import androidx.annotation.Nullable;
19+
import androidx.media3.common.util.Assertions;
1920
import androidx.media3.common.util.UnstableApi;
2021
import java.nio.ByteBuffer;
2122
import java.nio.ByteOrder;
@@ -49,6 +50,25 @@ public ByteBuffer init(long timeUs, int size) {
4950
return data;
5051
}
5152

53+
/**
54+
* Reallocates the buffer with new size
55+
* Existing data between beginning of the buffer and {@link ByteBuffer#limit} is copied to the new buffer,
56+
* and {@link ByteBuffer#position} is preserved. {@link ByteBuffer#limit} is set to the new size.
57+
* @param newSize New size of buffer.
58+
* @return The {@link #data} buffer, for convenience.
59+
*/
60+
public ByteBuffer grow(int newSize) {
61+
Assertions.checkNotNull(data);
62+
final ByteBuffer newData = ByteBuffer.allocateDirect(newSize).order(ByteOrder.nativeOrder());
63+
final int restorePosition = data.position();
64+
data.position(0);
65+
newData.put(data);
66+
newData.position(restorePosition);
67+
newData.limit(newSize);
68+
data = newData;
69+
return newData;
70+
}
71+
5272
@Override
5373
public void clear() {
5474
super.clear();

libraries/decoder_ffmpeg/proguard-rules.txt

+5
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44
-keepclasseswithmembernames class * {
55
native <methods>;
66
}
7+
8+
# This method is called from native code
9+
-keep class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder {
10+
private java.nio.ByteBuffer growOutputBuffer(androidx.media3.decoder.SimpleDecoderOutputBuffer, int);
11+
}

libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java

+18-10
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,16 @@
3232
/* package */ final class FfmpegAudioDecoder
3333
extends SimpleDecoder<DecoderInputBuffer, SimpleDecoderOutputBuffer, FfmpegDecoderException> {
3434

35-
// Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
36-
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
37-
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
35+
private static final int INITIAL_OUTPUT_BUFFER_SIZE_16BIT = 65535;
36+
private static final int INITIAL_OUTPUT_BUFFER_SIZE_32BIT = INITIAL_OUTPUT_BUFFER_SIZE_16BIT * 2;
3837

3938
private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
4039
private static final int AUDIO_DECODER_ERROR_OTHER = -2;
4140

4241
private final String codecName;
4342
@Nullable private final byte[] extraData;
4443
private final @C.PcmEncoding int encoding;
45-
private final int outputBufferSize;
44+
private int outputBufferSize;
4645

4746
private long nativeContext; // May be reassigned on resetting the codec.
4847
private boolean hasOutputFormat;
@@ -64,7 +63,7 @@ public FfmpegAudioDecoder(
6463
codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
6564
extraData = getExtraData(format.sampleMimeType, format.initializationData);
6665
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
67-
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
66+
outputBufferSize = outputFloat ? INITIAL_OUTPUT_BUFFER_SIZE_32BIT : INITIAL_OUTPUT_BUFFER_SIZE_16BIT;
6867
nativeContext =
6968
ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
7069
if (nativeContext == 0) {
@@ -107,8 +106,9 @@ protected FfmpegDecoderException decode(
107106
}
108107
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
109108
int inputSize = inputData.limit();
110-
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
111-
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
109+
outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
110+
111+
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputBuffer, outputBuffer.data, outputBufferSize);
112112
if (result == AUDIO_DECODER_ERROR_OTHER) {
113113
return new FfmpegDecoderException("Error decoding (see logcat).");
114114
} else if (result == AUDIO_DECODER_ERROR_INVALID_DATA) {
@@ -135,11 +135,19 @@ protected FfmpegDecoderException decode(
135135
}
136136
hasOutputFormat = true;
137137
}
138-
outputData.position(0);
139-
outputData.limit(result);
138+
outputBuffer.data.position(0);
139+
outputBuffer.data.limit(result);
140140
return null;
141141
}
142142

143+
// Called from native code
144+
/** @noinspection unused*/
145+
private ByteBuffer growOutputBuffer(SimpleDecoderOutputBuffer outputBuffer, int requiredSize) {
146+
// Use it for new buffer so that hopefully we won't need to reallocate again
147+
outputBufferSize = requiredSize;
148+
return outputBuffer.grow(requiredSize);
149+
}
150+
143151
@Override
144152
public void release() {
145153
super.release();
@@ -221,7 +229,7 @@ private native long ffmpegInitialize(
221229
int rawChannelCount);
222230

223231
private native int ffmpegDecode(
224-
long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize);
232+
long context, ByteBuffer inputData, int inputSize, SimpleDecoderOutputBuffer decoderOutputBuffer, ByteBuffer outputData, int outputSize);
225233

226234
private native int ffmpegGetChannelCount(long context);
227235

libraries/decoder_ffmpeg/src/main/jni/ffmpeg_jni.cc

+47-9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ extern "C" {
3535
#define LOG_TAG "ffmpeg_jni"
3636
#define LOGE(...) \
3737
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
38+
#define LOGD(...) \
39+
((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
3840

3941
#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \
4042
extern "C" { \
@@ -67,6 +69,8 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
6769
static const int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
6870
static const int AUDIO_DECODER_ERROR_OTHER = -2;
6971

72+
static jmethodID growOutputBufferMethod;
73+
7074
/**
7175
* Returns the AVCodec with the specified name, or NULL if it is not available.
7276
*/
@@ -81,13 +85,21 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
8185
jboolean outputFloat, jint rawSampleRate,
8286
jint rawChannelCount);
8387

88+
struct GrowOutputBufferCallback {
89+
uint8_t *operator()(int requiredSize) const;
90+
91+
JNIEnv *env;
92+
jobject thiz;
93+
jobject decoderOutputBuffer;
94+
};
95+
8496
/**
8597
* Decodes the packet into the output buffer, returning the number of bytes
8698
* written, or a negative AUDIO_DECODER_ERROR constant value in the case of an
8799
* error.
88100
*/
89101
int decodePacket(AVCodecContext *context, AVPacket *packet,
90-
uint8_t *outputBuffer, int outputSize);
102+
uint8_t *outputBuffer, int outputSize, GrowOutputBufferCallback growBuffer);
91103

92104
/**
93105
* Transforms ffmpeg AVERROR into a negative AUDIO_DECODER_ERROR constant value.
@@ -107,6 +119,17 @@ void releaseContext(AVCodecContext *context);
107119
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
108120
JNIEnv *env;
109121
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
122+
LOGE("JNI_OnLoad: GetEnv failed");
123+
return -1;
124+
}
125+
jclass clazz = env->FindClass("androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder");
126+
if (!clazz) {
127+
LOGE("JNI_OnLoad: FindClass failed");
128+
return -1;
129+
}
130+
growOutputBufferMethod = env->GetMethodID(clazz, "growOutputBuffer","(Landroidx/media3/decoder/SimpleDecoderOutputBuffer;I)Ljava/nio/ByteBuffer;");
131+
if (!growOutputBufferMethod) {
132+
LOGE("JNI_OnLoad: GetMethodID failed");
110133
return -1;
111134
}
112135
avcodec_register_all();
@@ -138,12 +161,12 @@ AUDIO_DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName,
138161
}
139162

140163
AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
141-
jint inputSize, jobject outputData, jint outputSize) {
164+
jint inputSize, jobject decoderOutputBuffer, jobject outputData, jint outputSize) {
142165
if (!context) {
143166
LOGE("Context must be non-NULL.");
144167
return -1;
145168
}
146-
if (!inputData || !outputData) {
169+
if (!inputData || !decoderOutputBuffer || !outputData) {
147170
LOGE("Input and output buffers must be non-NULL.");
148171
return -1;
149172
}
@@ -162,7 +185,17 @@ AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
162185
packet.data = inputBuffer;
163186
packet.size = inputSize;
164187
return decodePacket((AVCodecContext *)context, &packet, outputBuffer,
165-
outputSize);
188+
outputSize, GrowOutputBufferCallback{env, thiz, decoderOutputBuffer});
189+
}
190+
191+
uint8_t *GrowOutputBufferCallback::operator()(int requiredSize) const {
192+
jobject newOutputData = env->CallObjectMethod(thiz, growOutputBufferMethod, decoderOutputBuffer, requiredSize);
193+
if (env->ExceptionCheck()) {
194+
LOGE("growOutputBuffer() failed");
195+
env->ExceptionDescribe();
196+
return nullptr;
197+
}
198+
return static_cast<uint8_t *>(env->GetDirectBufferAddress(newOutputData));
166199
}
167200

168201
AUDIO_DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
@@ -264,7 +297,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
264297
}
265298

266299
int decodePacket(AVCodecContext *context, AVPacket *packet,
267-
uint8_t *outputBuffer, int outputSize) {
300+
uint8_t *outputBuffer, int outputSize, GrowOutputBufferCallback growBuffer) {
268301
int result = 0;
269302
// Queue input data.
270303
result = avcodec_send_packet(context, packet);
@@ -320,15 +353,20 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
320353
}
321354
context->opaque = resampleContext;
322355
}
323-
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
356+
324357
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
325358
int outSamples = swr_get_out_samples(resampleContext, sampleCount);
326359
int bufferOutSize = outSampleSize * channelCount * outSamples;
327360
if (outSize + bufferOutSize > outputSize) {
328-
LOGE("Output buffer size (%d) too small for output data (%d).",
361+
LOGD("Output buffer size (%d) too small for output data (%d), reallocating buffer.",
329362
outputSize, outSize + bufferOutSize);
330-
av_frame_free(&frame);
331-
return AUDIO_DECODER_ERROR_INVALID_DATA;
363+
outputSize = outSize + bufferOutSize;
364+
outputBuffer = growBuffer(outputSize);
365+
if (!outputBuffer) {
366+
LOGE("Failed to reallocate output buffer.");
367+
av_frame_free(&frame);
368+
return AUDIO_DECODER_ERROR_OTHER;
369+
}
332370
}
333371
result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
334372
(const uint8_t **)frame->data, frame->nb_samples);

0 commit comments

Comments
 (0)