Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ffmpeg: reallocate output buffer dynamically #746

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package androidx.media3.decoder;

import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
Expand Down Expand Up @@ -49,6 +50,27 @@ public ByteBuffer init(long timeUs, int size) {
return data;
}

/**
* Grows the buffer to a new size.
*
* <p>Existing data is copied to the new buffer, and {@link ByteBuffer#position} is preserved.
*
* @param newSize New size of the buffer.
* @return The {@link #data} buffer, for convenience.
*/
public ByteBuffer grow(int newSize) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add Javadoc to this method in the style of other existing Javadoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

ByteBuffer oldData = Assertions.checkNotNull(this.data);
Assertions.checkArgument(newSize >= oldData.limit());
ByteBuffer newData = ByteBuffer.allocateDirect(newSize).order(ByteOrder.nativeOrder());
int restorePosition = oldData.position();
oldData.position(0);
newData.put(oldData);
newData.position(restorePosition);
newData.limit(newSize);
this.data = newData;
return newData;
}

@Override
public void clear() {
super.clear();
Expand Down
5 changes: 5 additions & 0 deletions libraries/decoder_ffmpeg/proguard-rules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@
-keepclasseswithmembernames class * {
native <methods>;
}

# This method is called from native code
-keep class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder {
private java.nio.ByteBuffer growOutputBuffer(androidx.media3.decoder.SimpleDecoderOutputBuffer, int);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,16 @@
/* package */ final class FfmpegAudioDecoder
extends SimpleDecoder<DecoderInputBuffer, SimpleDecoderOutputBuffer, FfmpegDecoderException> {

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

private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
private static final int AUDIO_DECODER_ERROR_OTHER = -2;

private final String codecName;
@Nullable private final byte[] extraData;
private final @C.PcmEncoding int encoding;
private final int outputBufferSize;
private int outputBufferSize;

private long nativeContext; // May be reassigned on resetting the codec.
private boolean hasOutputFormat;
Expand All @@ -64,7 +63,8 @@ public FfmpegAudioDecoder(
codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
outputBufferSize =
outputFloat ? INITIAL_OUTPUT_BUFFER_SIZE_32BIT : INITIAL_OUTPUT_BUFFER_SIZE_16BIT;
nativeContext =
ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
if (nativeContext == 0) {
Expand Down Expand Up @@ -108,7 +108,9 @@ protected FfmpegDecoderException decode(
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
int result =
ffmpegDecode(
nativeContext, inputData, inputSize, outputBuffer, outputData, outputBufferSize);
if (result == AUDIO_DECODER_ERROR_OTHER) {
return new FfmpegDecoderException("Error decoding (see logcat).");
} else if (result == AUDIO_DECODER_ERROR_INVALID_DATA) {
Expand Down Expand Up @@ -140,6 +142,14 @@ protected FfmpegDecoderException decode(
return null;
}

// Called from native code
@SuppressWarnings("unused")
private ByteBuffer growOutputBuffer(SimpleDecoderOutputBuffer outputBuffer, int requiredSize) {
// Use it for new buffer so that hopefully we won't need to reallocate again
outputBufferSize = requiredSize;
return outputBuffer.grow(requiredSize);
}

@Override
public void release() {
super.release();
Expand Down Expand Up @@ -221,7 +231,12 @@ private native long ffmpegInitialize(
int rawChannelCount);

private native int ffmpegDecode(
long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize);
long context,
ByteBuffer inputData,
int inputSize,
SimpleDecoderOutputBuffer decoderOutputBuffer,
ByteBuffer outputData,
int outputSize);

private native int ffmpegGetChannelCount(long context);

Expand Down
56 changes: 47 additions & 9 deletions libraries/decoder_ffmpeg/src/main/jni/ffmpeg_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ extern "C" {
#define LOG_TAG "ffmpeg_jni"
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#define LOGD(...) \
((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))

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

static jmethodID growOutputBufferMethod;

/**
* Returns the AVCodec with the specified name, or NULL if it is not available.
*/
Expand All @@ -81,13 +85,21 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
jboolean outputFloat, jint rawSampleRate,
jint rawChannelCount);

struct GrowOutputBufferCallback {
uint8_t *operator()(int requiredSize) const;

JNIEnv *env;
jobject thiz;
jobject decoderOutputBuffer;
};

/**
* Decodes the packet into the output buffer, returning the number of bytes
* written, or a negative AUDIO_DECODER_ERROR constant value in the case of an
* error.
*/
int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize);
uint8_t *outputBuffer, int outputSize, GrowOutputBufferCallback growBuffer);

/**
* Transforms ffmpeg AVERROR into a negative AUDIO_DECODER_ERROR constant value.
Expand All @@ -107,6 +119,17 @@ void releaseContext(AVCodecContext *context);
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE("JNI_OnLoad: GetEnv failed");
return -1;
}
jclass clazz = env->FindClass("androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you reference classes and methods by name from native code, you'll also need to add them to the proguard-rules.txtfile of the ffmpeg module to prevent them from being obfuscated during the build process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (!clazz) {
LOGE("JNI_OnLoad: FindClass failed");
return -1;
}
growOutputBufferMethod = env->GetMethodID(clazz, "growOutputBuffer","(Landroidx/media3/decoder/SimpleDecoderOutputBuffer;I)Ljava/nio/ByteBuffer;");
if (!growOutputBufferMethod) {
LOGE("JNI_OnLoad: GetMethodID failed");
return -1;
}
avcodec_register_all();
Expand Down Expand Up @@ -138,12 +161,12 @@ AUDIO_DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName,
}

AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
jint inputSize, jobject outputData, jint outputSize) {
jint inputSize, jobject decoderOutputBuffer, jobject outputData, jint outputSize) {
if (!context) {
LOGE("Context must be non-NULL.");
return -1;
}
if (!inputData || !outputData) {
if (!inputData || !decoderOutputBuffer || !outputData) {
LOGE("Input and output buffers must be non-NULL.");
return -1;
}
Expand All @@ -162,7 +185,17 @@ AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
packet.data = inputBuffer;
packet.size = inputSize;
return decodePacket((AVCodecContext *)context, &packet, outputBuffer,
outputSize);
outputSize, GrowOutputBufferCallback{env, thiz, decoderOutputBuffer});
}

uint8_t *GrowOutputBufferCallback::operator()(int requiredSize) const {
jobject newOutputData = env->CallObjectMethod(thiz, growOutputBufferMethod, decoderOutputBuffer, requiredSize);
if (env->ExceptionCheck()) {
LOGE("growOutputBuffer() failed");
env->ExceptionDescribe();
return nullptr;
}
return static_cast<uint8_t *>(env->GetDirectBufferAddress(newOutputData));
}

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

int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize) {
uint8_t *outputBuffer, int outputSize, GrowOutputBufferCallback growBuffer) {
int result = 0;
// Queue input data.
result = avcodec_send_packet(context, packet);
Expand Down Expand Up @@ -320,15 +353,20 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
}
context->opaque = resampleContext;
}
int inSampleSize = av_get_bytes_per_sample(sampleFormat);

int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
int outSamples = swr_get_out_samples(resampleContext, sampleCount);
int bufferOutSize = outSampleSize * channelCount * outSamples;
if (outSize + bufferOutSize > outputSize) {
LOGE("Output buffer size (%d) too small for output data (%d).",
LOGD("Output buffer size (%d) too small for output data (%d), reallocating buffer.",
outputSize, outSize + bufferOutSize);
av_frame_free(&frame);
return AUDIO_DECODER_ERROR_INVALID_DATA;
outputSize = outSize + bufferOutSize;
outputBuffer = growBuffer(outputSize);
if (!outputBuffer) {
LOGE("Failed to reallocate output buffer.");
av_frame_free(&frame);
return AUDIO_DECODER_ERROR_OTHER;
}
}
result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
(const uint8_t **)frame->data, frame->nb_samples);
Expand Down