310 lines
9.2 KiB
Markdown
310 lines
9.2 KiB
Markdown
+++
|
|
template = "article.html"
|
|
title = "Compile FFmpeg for Android"
|
|
date = 2014-06-20T10:40:00+02:00
|
|
description = "A comprehensive guide to compiling FFmpeg for Android, including building the necessary toolchain and integrating it into your project."
|
|
|
|
[taxonomies]
|
|
tags = ["android"]
|
|
+++
|
|
|
|
When you have to manipulate audio or video on Android, being used to open-source
|
|
software, you have a single name which comes directly to you: FFmpeg. However,
|
|
FFmpeg is a C software, meant to be used as an executable, and not officially
|
|
supporting Android.
|
|
|
|
There are a lot of partial and/or out-of-date how-to out there on how to get
|
|
FFmpeg running on Android, like
|
|
[halfninja's build](https://github.com/halfninja/android-ffmpeg-x264). However,
|
|
I needed to use FFmpeg `concat` demuxer, introduced in FFmpeg 1.1. Most builds
|
|
target 0.9. There's
|
|
[a ton](http://stackoverflow.com/search?q=ffmpeg+android)
|
|
of questions on StackOverflow about getting newer
|
|
FFmpeg releases working on Android. So, here's a full explanation to get
|
|
[FFmpeg 2.2.3 "Muybridge"](https://www.ffmpeg.org/releases/ffmpeg-2.2.3.tar.bz2)
|
|
working on Android. I'll describe the steps for Linux, but everything is pretty
|
|
standard shell and should work on any decent OS.
|
|
|
|
<!--more-->
|
|
|
|
## Prerequisites
|
|
|
|
First, let's install everything needed.
|
|
|
|
### Android SDK and NDK
|
|
|
|
Android SDK is available [here](http://developer.android.com/sdk/index.html)
|
|
while the NDK is available
|
|
[here](https://developer.android.com/tools/sdk/ndk/index.html). You should also
|
|
set two environment variables (`ANDROID_SDK` and `ANDROID_NDK`) to their
|
|
respective installation paths.
|
|
|
|
On Archlinux, using `android-sdk` and `android-ndk` AUR packages:
|
|
|
|
{{ filename(body="Setting environment variables for Android SDK/NDK") }}
|
|
|
|
```sh
|
|
export ANDROID_NDK=/opt/android-ndk/
|
|
export ANDROID_SDK=/opt/android-sdk/
|
|
```
|
|
|
|
### FFmpeg sources
|
|
|
|
Download FFmpeg sources
|
|
[here](https://www.ffmpeg.org/releases/ffmpeg-2.2.3.tar.bz2) and extract them in
|
|
`$ANDROID_NDK/sources/ffmpeg-2.2.3`. Building third-party libraries in
|
|
`$ANDROID_NDK/sources` make them easily available to use in other projects.
|
|
|
|
## Building FFmpeg
|
|
|
|
### Configuration
|
|
|
|
You can tweak the configuration if needed, but here's the one I used:
|
|
|
|
{{ filename(body="FFmpeg configuration") }}
|
|
|
|
```sh
|
|
SYSROOT=$ANDROID_NDK/platforms/android-9/arch-arm/
|
|
# You should adjust this path depending on your platform, e.g. darwin-x86_64 for Mac OS
|
|
TOOLCHAIN=$ANDROID_NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
|
|
CPU=arm
|
|
PREFIX=$(pwd)/android/$CPU
|
|
|
|
# Set these if needed
|
|
ADDI_CFLAGS=""
|
|
ADDI_LDFLAGS=""
|
|
|
|
./configure \
|
|
--prefix=$PREFIX \
|
|
--disable-shared \
|
|
--enable-static \
|
|
--disable-doc \
|
|
--disable-ffmpeg \
|
|
--disable-ffplay \
|
|
--disable-ffprobe \
|
|
--disable-ffserver \
|
|
--disable-doc \
|
|
--disable-symver \
|
|
--enable-protocol=concat \
|
|
--enable-protocol=file \
|
|
--enable-muxer=mp4 \
|
|
--enable-demuxer=mpegts \
|
|
--enable-memalign-hack \
|
|
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
|
|
--target-os=linux \
|
|
--arch=arm \
|
|
--enable-cross-compile \
|
|
--sysroot=$SYSROOT \
|
|
--extra-cflags="-Os -fpic -marm $ADDI_CFLAGS" \
|
|
--extra-ldflags="$ADDI_LDFLAGS"
|
|
```
|
|
|
|
### Compilation
|
|
|
|
The scariest step is in fact the simplest:
|
|
|
|
{{ filename(body="FFmpeg compilation") }}
|
|
|
|
```sh
|
|
make clean
|
|
# Adapt the jobs count to your machine
|
|
make -j3
|
|
make install
|
|
```
|
|
|
|
### Expose FFmpeg to Android NDK
|
|
|
|
To be able to use FFmpeg as a usual NDK module, we need an `Android.mk`. It
|
|
should be placed in `$ANDROID_NDK/sources/ffmpeg-2.2.3/android/arm`.
|
|
|
|
{{ filename(body="Android.mk") }}
|
|
|
|
```make
|
|
LOCAL_PATH:= $(call my-dir)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libavdevice
|
|
LOCAL_SRC_FILES:= lib/libavdevice.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libavcodec
|
|
LOCAL_SRC_FILES:= lib/libavcodec.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libavformat
|
|
LOCAL_SRC_FILES:= lib/libavformat.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libswscale
|
|
LOCAL_SRC_FILES:= lib/libswscale.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libavutil
|
|
LOCAL_SRC_FILES:= lib/libavutil.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libavfilter
|
|
LOCAL_SRC_FILES:= lib/libavfilter.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
|
|
include $(CLEAR_VARS)
|
|
LOCAL_MODULE:= libswresample
|
|
LOCAL_SRC_FILES:= lib/libswresample.a
|
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
include $(PREBUILT_STATIC_LIBRARY)
|
|
```
|
|
|
|
That's it! FFmpeg is ready to use!
|
|
|
|
## Using FFmpeg
|
|
|
|
To use FFmpeg, I'll stick to [halfninja](https://github.com/halfninja)'s
|
|
idea: adapt FFmpeg's `main()` to a simple function, and write a JNI
|
|
interface around it. A sample project is available on
|
|
[GitHub](https://github.com/Kernald/ffmpeg-android).
|
|
|
|
### Adapting FFmpeg's `main()`
|
|
|
|
I used some FFmpeg's executable source files (`ffmpeg.c`, containing `main()`,
|
|
and directly related ones), and tweaked them: removed every `exit()` call and
|
|
replaced `av_log()` calls to use Android's LogCat. As FFmpeg's executable is
|
|
meant to be run once, then exited, I also needed to reinitialize some static
|
|
variables between every `main()` calls.
|
|
|
|
<!-- markdownlint-capture -->
|
|
<!-- markdownlint-disable MD051 -->
|
|
_Update from March 27th 2016_: for an up-to-date sample, see
|
|
[this GitHub repository](https://github.com/HikoQiu/JNI_INVOKE_FFMPEG/blob/master/jni/ffmpeg.c#L4122).
|
|
Thanks Hiko!
|
|
<!-- markdownlint-restore -->
|
|
|
|
### JNI interface
|
|
|
|
The JNI interface is really simple: a simple C wrapper calling FFmpeg's
|
|
`main()`, and a Java wrapper around it.
|
|
|
|
Here's the C function, excluding usual JNI boilerplate (complete file is
|
|
available on GitHub):
|
|
|
|
{{ filename(body="JNI C wrapper") }}
|
|
|
|
```c
|
|
JNIEXPORT jboolean JNICALL Java_fr_enoent_videokit_Videokit_run(JNIEnv *env, jobject obj, jobjectArray args) {
|
|
int i = 0;
|
|
int argc = 0;
|
|
char **argv = NULL;
|
|
jstring *strr = NULL;
|
|
|
|
if (args != NULL) {
|
|
argc = (*env)->GetArrayLength(env, args);
|
|
argv = (char **) malloc(sizeof(char *) * argc);
|
|
strr = (jstring *) malloc(sizeof(jstring) * argc);
|
|
|
|
for (i = 0; i < argc; ++i) {
|
|
strr[i] = (jstring)(*env)->GetObjectArrayElement(env, args, i);
|
|
argv[i] = (char *)(*env)->GetStringUTFChars(env, strr[i], 0);
|
|
LOGI("Option: %s", argv[i]);
|
|
}
|
|
}
|
|
|
|
LOGI("Running main");
|
|
int result = main(argc, argv);
|
|
LOGI("Main ended with status %d", result);
|
|
|
|
for (i = 0; i < argc; ++i) {
|
|
(*env)->ReleaseStringUTFChars(env, strr[i], argv[i]);
|
|
}
|
|
free(argv);
|
|
free(strr);
|
|
|
|
return result == 0;
|
|
}
|
|
```
|
|
|
|
The function simply takes JNI arguments (`jobject obj` and `jobjectArray args`)
|
|
and creates matching `char*` parameters. These parameters are then passed to
|
|
FFmpeg's `main()`. It then returns `true` if everything was fine (FFmpeg
|
|
returned `0`), `false` otherwise.
|
|
|
|
The Java part is even simpler. Once again, only the interesting part:
|
|
|
|
{{ filename(body="JNI Java wrapper") }}
|
|
|
|
```java
|
|
package fr.enoent.videokit;
|
|
|
|
public final class Videokit {
|
|
|
|
// Truncated library loading, see complete file on GitHub
|
|
|
|
/**
|
|
* Call FFmpeg with specified arguments
|
|
* @param args FFmpeg arguments
|
|
* @return true if success, false otherwise
|
|
*/
|
|
public boolean process(String[] args) {
|
|
String[] params = new String[args.length + 1];
|
|
params[0] = "ffmpeg";
|
|
System.arraycopy(args, 0, params, 1, args.length);
|
|
|
|
return run(params);
|
|
}
|
|
|
|
private native boolean run(String[] args);
|
|
}
|
|
```
|
|
|
|
The native `run()` method is pretty obvious: it simply calls the previous C
|
|
function. However, FFmpeg's `main()` expects to see the executable name as its
|
|
first parameter. Even if we don't compile it as an executable file, I found it
|
|
simpler to add this parameter than modifying FFmpeg code to not use it. Hence,
|
|
the `process()` method, which is the only public interface to call FFmpeg. It
|
|
simply adds `ffmpeg` as first parameter, then calls `run()`.
|
|
|
|
### Call FFmpeg from Java
|
|
|
|
Once we have the JNI wrapper in place, calling FFmpeg from Java code is really
|
|
straightforward. Here's a sample call which trims the video available on
|
|
`/sdcard/input.mp4` to keep only 15 seconds of it, and write the result to
|
|
`/sdcard/output.mp4`:
|
|
|
|
{{ filename(body="Using FFmpeg") }}
|
|
|
|
```java
|
|
if (Videokit.getInstance().process(new String[] {
|
|
"-y", // Overwrite output files
|
|
"-i", // Input file
|
|
"/sdcard/input.mp4",
|
|
"-ss", // Start position
|
|
"0",
|
|
"-t", // Duration
|
|
"15",
|
|
"-vcodec", // Video codec
|
|
"copy",
|
|
"-acodec", // Audio codec
|
|
"copy",
|
|
"/sdcard/output.mp4" // Output file
|
|
)) {
|
|
Log.d(TAG, "Trimming: success");
|
|
} else {
|
|
Log.d(TAG, "Trimming: failure");
|
|
}
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
While using FFmpeg on Android is really useful when dealing with audio and video
|
|
files, it wasn't as easy as one could think to get it working the first time,
|
|
with an up-to-date FFmpeg version. However, once set up, it works great, with
|
|
decent performances even on mid-end hardware.
|