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

Conversation

equeim
Copy link
Contributor

@equeim equeim commented Oct 19, 2023

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.

@tonihei
Copy link
Collaborator

tonihei commented Oct 20, 2023

Thanks for the PR! I added a few comments for improvement ideas.

@tonihei tonihei self-assigned this Oct 20, 2023
@equeim
Copy link
Contributor Author

equeim commented Oct 20, 2023

Thanks for the PR! I added a few comments for improvement ideas.

Where? 🤔 @tonihei

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.

@@ -49,6 +50,18 @@ public ByteBuffer init(long timeUs, int size) {
return data;
}

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.

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).",
LOGE("Output buffer size (%d) too small for output data (%d), reallocating buffer.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't need to be LOGE anymore if the case is handled gracefully, we could change it to LOGD

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.

outputSize = outSize + bufferOutSize;
outputBuffer = growBuffer(outputSize);
if (outputBuffer) {
LOGE("Reallocated output buffer.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need for this log line either

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 outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
Assertions.checkNotNull(outputBuffer.data);
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can leave out this check as this is guaranteed by the API contract of the init method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, but then IDE paints outputBuffer.data on the next line yellow.

return null;
}

// Called from native code
@Keep
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't use this annotation anywhere else at the moment. I'm not sure, but I think the proguard rules I asked about in another comment should be sufficient to keep the method.

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.

@tonihei
Copy link
Collaborator

tonihei commented Oct 30, 2023

Where? 🤔 @tonihei

Oops, sorry, I forgot to click on "publish review"!

@equeim equeim force-pushed the ffmpeg-output-buffer-reallocation branch from fe4bd76 to a66683a Compare October 30, 2023 16:23
@tonihei
Copy link
Collaborator

tonihei commented Oct 31, 2023

Thanks! I'll now import this PR internally and you may see some further clean-up commits appear here.

equeim and others added 2 commits October 31, 2023 12:25
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.
Also amended Javadoc and added assertion to grow method
@tonihei tonihei force-pushed the ffmpeg-output-buffer-reallocation branch from a66683a to 8750ed8 Compare October 31, 2023 12:28
@copybara-service copybara-service bot merged commit 4f99000 into androidx:main Oct 31, 2023
1 check passed
@Tolriq
Copy link
Contributor

Tolriq commented Nov 1, 2023

@tonihei Your commit 8750ed8 breaks this.

By using a variable to hold the buffer you keep a reference to the old buffer at the end of the function.

So that call outputData.limit(result); fails as the result value can be related to the new buffer size but outputData is still the lowersize buffer and you have Caused by: java.lang.IllegalArgumentException: newLimit > capacity: (73728 > 65535) errors.

@tonihei
Copy link
Collaborator

tonihei commented Nov 2, 2023

Oh yes, sorry, that was clearly too much "clean-up" :) Will send a fix commit straight-away.

@tonihei
Copy link
Collaborator

tonihei commented Nov 2, 2023

Should be fixed by ae6f83d

@Tolriq
Copy link
Contributor

Tolriq commented Nov 2, 2023

@tonihei yes the fix works but the keep rules are not working :(
We need to also keep SimpleDecoderOutputBuffer else it's renamed and eventually merged and the GetMethodID fails.

@tonihei
Copy link
Collaborator

tonihei commented Nov 2, 2023

Do you want to send another PR given you can actually test this out? I was working by looking on the code only without an actual test case.

@Tolriq
Copy link
Contributor

Tolriq commented Nov 2, 2023

@tonihei In this case just compile any app with ffmpeg with R8 and the extension will fail to load in the JNI_OnLoad so no need for a file to repro.

And I can send you a file that repro this by mail if you want for future reference too.

For the PR even after all those years I'm still far from a proguard/R8 expert.

So -keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; } Do work and fix this, but there's probably a more targeted solution to this.

As bonus questions since there's movement in that extension: Is there any chance that #707 be considered? And if yes is there any interest in a PR that adds resampling to ffmpeg extension so that hi res media that Android can't play can properly be downsampled to the max supported sample rate ?

@tonihei
Copy link
Collaborator

tonihei commented Nov 2, 2023

Yes, if you could send a file that needs this logic to android-media-github@google.com with "Issue #746" in the subject, that would be great so that I can just test it myself.

@Tolriq
Copy link
Contributor

Tolriq commented Nov 3, 2023

And just in case I was not clear enough @tonihei ffmpeg extension in main is broken for any file in current state if you build with R8 as it fails to load the extension at all.

@tonihei
Copy link
Collaborator

tonihei commented Nov 3, 2023

@Tolriq I can't reproduce the R8 failure I'm afraid. I'm testing on the latest main branch demo app in release mode with your example file. And everything works as expected (including the automated buffer resizing).

The existing proguard rule seems to ensure that the SimpleDecoderOutputBuffer class is kept already. I added

-whyareyoukeeping class androidx.media3.decoder.SimpleDecoderOutputBuffer

to get an explanation for why this class is kept in my build and the output directly references the existing rule:

androidx.media3.decoder.SimpleDecoderOutputBuffer
|- is referenced from:
|  java.nio.ByteBuffer androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder.growOutputBuffer(androidx.media3.decoder.SimpleDecoderOutputBuffer,int)
|- is referenced in keep rule:
|  [...]/media/libraries/decoder_ffmpeg/buildout/intermediates/consumer_proguard_dir/release/lib0/proguard.txt:9:1

Could you check that the Ffmpeg proguard rules are included in your build? (maybe you need to clean caches/builds and try again?)

@Tolriq
Copy link
Contributor

Tolriq commented Nov 3, 2023

@tonihei The rules are included I'm using R8 with full mode and version "8.2.34"

In that mode the SimpleDecoderOutputBuffer is renamed and merged leading to the issue.

Maybe it's an R8 issue and the fact that the class is passed as a param should ensure it's kept, but from my understanding of R8/Proguard there's no guarantee about that with the current rule.

The rule

-keep class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder {
  private java.nio.ByteBuffer growOutputBuffer(androidx.media3.decoder.SimpleDecoderOutputBuffer, int);
}

Just implies that the class and function is kept nothing about the parameters.

And in the code we do reflection with

  growOutputBufferMethod =
      env->GetMethodID(clazz, "growOutputBuffer",
                       "(Landroidx/media3/decoder/"
                       "SimpleDecoderOutputBuffer;I)Ljava/nio/ByteBuffer;");

That explicitly require the type and name to be kept.

I can open a question on R8 team about this, and come back here if you want their confirmation.

Edit: Opened https://issuetracker.google.com/issues/309068090 they are very efficient so we'll have an answer soon.

@tonihei
Copy link
Collaborator

tonihei commented Nov 3, 2023

Let me try the full mode too (that wasn't enabled in my test I believe)

@tonihei
Copy link
Collaborator

tonihei commented Nov 3, 2023

Thanks for following up on the issue with R8. I still can't reproduce it in any way though. I upgraded to R8 8.2.33 (as part of the Android Studio Gradle plugin 8.2.0-rc02), but can't see the issue with or without full mode. Not sure where the difference is to be honest.

Could I ask you to test if includedescriptorclasses works in your setup? If yes, I'll submit this change to fix this.

@Tolriq
Copy link
Contributor

Tolriq commented Nov 3, 2023

@tonihei yes it works with:

# This method is called from native code
-keep, includedescriptorclasses class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder {
  private java.nio.ByteBuffer growOutputBuffer(androidx.media3.decoder.SimpleDecoderOutputBuffer, int);
}

@tonihei
Copy link
Collaborator

tonihei commented Nov 3, 2023

Great, will send a fix for this then! Thanks for your help :)

@Tolriq
Copy link
Contributor

Tolriq commented Nov 3, 2023

May I ask again some help on #369 then ? ;) Kills me to no be able to provide the test file to get this merged.

@tonihei
Copy link
Collaborator

tonihei commented Nov 6, 2023

May I ask again some help on #369 then ?

I think this fell through the cracks a bit because we were waiting to see if there is a way to produce a test file for unit tests.

@androidx androidx locked and limited conversation to collaborators Dec 31, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants