воскресенье, 19 сентября 2010 г.

Собираем ffmpeg для Android

Недавно возникла задача по генерации видео в Android. Стандартных средств для этого в SDK нет, так что пришлось искать сторонюю библиотеку. В конечном счете пришлось использовать ffmpeg.

ffmpeg — это, в первую очередь, очень удобная консольная утилита для работы с видео, и только потом библиотека, которую можно использовать в своих проектах. Соответственно, и информации по консольной утилите в разы больше. А если учесть, что код ffmpeg, хоть и открытый, но написан так, что черт в нем ногу сломит, то использование данной библиотеки превращается в занятие просто самоубийственное.
Но речь сегодня будет не про то, как юзать ffmpeg. Мы пишем под Android, и поэтому нам нужно сначала собрать библиотеку в пригодный для него вид. Дело в том, что ffmpeg написан на чистом C, и иначе, чем через Native Development Tools, использовать его не получится.

Требования

  • Какой-нибудь Linux. В моем случае был Ubuntu на VirtualBox. Я убежденный адепт Майкрософта, но на 64-битной винде мне ничего не удалось.
  • Android SDK
  • Android NDK r04b. Кстати, неплохо бы иметь о нем некоторое представление.
  • Eclipse

Процесс

Создаем проект

Создаем проект для Android в Eclipse, добавляем в него папку jni.

Получаем исходники ffmpeg

Тут есть два подхода. Можно слить исходники из svn командой svn checkout svn://svn.ffmpeg.org/ffmpeg/trunk ffmpeg. А можно взять версию 0.6. ffmpeg довольно быстро развивается, так что разница будет существенна. В данной статье я буду рассматривать процесс сборки из транка, т.к. он меняется, а 0.6 просто выложу собранным.
Итак, слили исходники и положили их в папку jni/ffmpeg нашего проекта.
Структура проекта

Конфигурируем ffmpeg

В состав ffmpeg входит shell-скрипт configure, который способен сгенерировать .h и make файлы для компиляции ffmpeg.
configure имеет множество ключей, от которых зависит какие компоненты будут входить в вашу сборку ffmpeg. Полный список настроек можно получить, набрав команду:
./configure --help
Ключей для запуска будет много, так что сделаем отдельный скрипт config.sh для запуска:

config.sh

PREBUILT=/home/darja/android-ndk/build/prebuilt/linux-x86/arm-eabi-4.4.0
PLATFORM=/home/darja/android-ndk/build/platforms/android-3/arch-arm

./configure --target-os=linux \
    --arch=arm \
    --enable-nonfree \
    --disable-protocols \
    --enable-protocol=file \
    --disable-network \
    --enable-avfilter \
    --enable-cross-compile \
    --cc=$PREBUILT/bin/arm-eabi-gcc \
    --cross-prefix=$PREBUILT/bin/arm-eabi- \
    --nm=$PREBUILT/bin/arm-eabi-nm \
    --extra-cflags="-fPIC -DANDROID" \
    --disable-asm \
    --enable-neon \
    --enable-armv5te \
    --extra-ldflags="-Wl,-T,$PREBUILT/arm-eabi/lib/ldscripts/armelf.x -Wl,-rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -nostdlib $PREBUILT/lib/gcc/arm-eabi/4.4.0/crtbegin.o $PREBUILT/lib/gcc/arm-eabi/4.4.0/crtend.o -lc -lm -ldl"
Пути PREBUILT и PLATFORM, само собой, следует заменить на свои.
Выполняем config.sh, получаем большой отчет о сконфигурированных для нас частях ffmpeg.
Результат работы config.sh

Адаптируем к NDK

Скрипт нагенерировал нам кучу файликов. Если бы мы писали под Linux, то делать бы ничего не пришлось. Но у нас Android, и поэтому нужно кое-что допилить.
  1. В файле /jni/ffmpeg/config.h поменять #define restrict restrict на #define restrict.
  2. В файле /jni/ffmpeg/libavutil/libm.h убрать все static-функции.
  3. В папках libavcodec, libavcore, libavfilter, libavformat, libavutil, libpostproc, libswscale из всех Makefile удалить строки:

    include $(SUBDIR)../subdir.mak
    include $(SUBDIR)../config.mak
  4. Поместить в /jni/ffmpeg файл av.mk с текстом:


    /jni/ffmpeg/av.mk

    include $(LOCAL_PATH)/../config.mak
    
    OBJS :=
    OBJS-yes :=
    MMX-OBJS-yes :=
    include $(LOCAL_PATH)/Makefile
    
    # collect objects
    OBJS-$(HAVE_MMX) += $(MMX-OBJS-yes)
    OBJS += $(OBJS-yes)
    
    FFNAME := lib$(NAME)
    FFLIBS := $(foreach,NAME,$(FFLIBS),lib$(NAME))
    FFCFLAGS  = -DHAVE_AV_CONFIG_H -Wno-sign-compare -Wno-switch -Wno-pointer-sign
    FFCFLAGS += -DTARGET_CONFIG=\"config-$(TARGET_ARCH).h\"
    
    ALL_S_FILES := $(wildcard $(LOCAL_PATH)/$(TARGET_ARCH)/*.S)
    ALL_S_FILES := $(addprefix $(TARGET_ARCH)/, $(notdir $(ALL_S_FILES)))
    
    ifneq ($(ALL_S_FILES),)
    ALL_S_OBJS := $(patsubst %.S,%.o,$(ALL_S_FILES))
    C_OBJS := $(filter-out $(ALL_S_OBJS),$(OBJS))
    S_OBJS := $(filter $(ALL_S_OBJS),$(OBJS))
    else
    C_OBJS := $(OBJS)
    S_OBJS :=
    endif
    
    C_FILES := $(patsubst %.o,%.c,$(C_OBJS))
    S_FILES := $(patsubst %.o,%.S,$(S_OBJS))
    
    FFFILES := $(sort $(S_FILES)) $(sort $(C_FILES))
    
  5. Создать в /jni/ файл Android.mk:

    /jni/Android.mk

    include $(all-subdir-makefiles)
    
  6. Создать в /jni/ffmpeg файл Android.mk:

    /jni/ffmpeg/Android.mk

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_STATIC_LIBRARIES := libavcore libavformat libavcodec libavutil libpostproc libswscale
    LOCAL_MODULE := ffmpeg
    include $(BUILD_SHARED_LIBRARY)
    include $(call all-makefiles-under,$(LOCAL_PATH))
    
  7. Создать в /jni/ffmpeg/libavformat файл Android.mk:

    /jni/ffmpeg/libavformat/Android.mk

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    include $(LOCAL_PATH)/../av.mk
    LOCAL_SRC_FILES := $(FFFILES)
    LOCAL_C_INCLUDES :=     \
        $(LOCAL_PATH)     \
        $(LOCAL_PATH)/..
    LOCAL_CFLAGS += $(FFCFLAGS)
    LOCAL_CFLAGS += -include "string.h" -Dipv6mr_interface=ipv6mr_ifindex
    LOCAL_LDLIBS := -lz
    LOCAL_STATIC_LIBRARIES := $(FFLIBS)
    LOCAL_MODULE := $(FFNAME)
    include $(BUILD_STATIC_LIBRARY)
    
  8. Создать в /jni/ffmpeg/libavcodec файл Android.mk

    /jni/ffmpeg/libavcodec/Android.mk

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    include $(LOCAL_PATH)/../av.mk
    LOCAL_SRC_FILES := $(FFFILES)
    LOCAL_C_INCLUDES :=     \
        $(LOCAL_PATH)     \
        $(LOCAL_PATH)/..
    LOCAL_CFLAGS += $(FFCFLAGS)
    LOCAL_LDLIBS := -lz
    LOCAL_STATIC_LIBRARIES := $(FFLIBS)
    LOCAL_MODULE := $(FFNAME)
    include $(BUILD_STATIC_LIBRARY)
    
  9. Поместить в папки libavcore (к слову, в релизе такой папки нет), libavfilter, libavutil, libpostproc, libswscale по файлу Android.mk

    Android.mk

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    include $(LOCAL_PATH)/../av.mk
    LOCAL_SRC_FILES := $(FFFILES)
    LOCAL_C_INCLUDES :=     \
        $(LOCAL_PATH)     \
        $(LOCAL_PATH)/..
    LOCAL_CFLAGS += $(FFCFLAGS)
    LOCAL_STATIC_LIBRARIES := $(FFLIBS)
    LOCAL_MODULE := $(FFNAME)
    include $(BUILD_STATIC_LIBRARY)
    
    

Компилируем

Из корня проекта запускаем скрипт ndk-build (он находится в NDK) и идем пить чай, потому что компилироваться будет долго.
Когда компиляция будет окончена, идем в папку obj и видим:
Содержимое папки obj
Собственно, эти файлы нам и нужны.

А дальше?

Теперь кратко о том, что с этими файлами делать. Переносим папку obj в проект, в котором планируется использовать ffmpeg (на этом месте можно возвращаться обратно в Windows). Добавляем также папку jni, в которой будет находиться наш код для NDK. Также нужно будет добавить заголовочные файлы ffmpeg. Получится примерно вот такая картина:
Структура проекта, использующего ffmpeg
Дабы убедиться, что ffmpeg действительно подключается и работает, напишем какой-нибудь код. Создадим в jni файл mylib.c:

mylib.c

#include <jni.h>
#include <android/log.h>

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>

#define LOG_TAG "mylib"
#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

jint Java_demo_ffmpeg_MainActivity_logFileInfo(JNIEnv * env, jobject this, jstring filename)
{
    av_register_all();

    AVFormatContext *pFormatCtx;
    const jbyte *str;
    str = (*env)->GetStringUTFChars(env, filename, NULL);

    if(av_open_input_file(&pFormatCtx, str, NULL, 0, NULL)!=0)
    {
        LOGE("Can't open file '%s'\n", str);
        return 1;
    }
    else
    {
        LOGI("File was opened\n");
        LOGI("File '%s', Codec %s",
            pFormatCtx->filename,
            pFormatCtx->iformat->name
        );
    }
    return 0;
}
И добавим makefile:

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_LDLIBS += -llog
LOCAL_STATIC_LIBRARIES := libavformat libavcodec libpostproc libswscale libavutil libavcore 
LOCAL_C_INCLUDES += $(LOCAL_PATH)/ffmpeg
LOCAL_SRC_FILES := mylib.c
LOCAL_MODULE    := mylib

include $(BUILD_SHARED_LIBRARY)

LOCAL_PATH := $(call my-dir)
LOCAL_C_INCLUDES += $(LOCAL_PATH)/ffmpeg
include $(all-subdir-makefiles)
Из папки jni вызовем команду ndk-build. В проекте появится папка libs, в которой окажется библиотека libmylib.so. Теперь можно вызывать функции нашей библиотеки в java-коде. Конечно, это уже детали NDK, но для полноты картины приведу и этот код:

MainActivity.java

public class MainActivity extends Activity
{
    private static native int logFileInfo(String filename);

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);        
        setContentView(R.layout.main);
        
        logFileInfo("/sdcard/sample.mpg");
    }

    static
    {
        System.loadLibrary("mylib");
    }
}
Когда запустим этот код, в LogCat выведется что-то вроде:
09-19 15:47:49.379: INFO/mylib(851): File was opened
09-19 15:47:49.379: INFO/mylib(851): File '/sdcard/sample.mpg', Codec mpeg
Как видно, ffmpeg работает.

Заключение

Надеюсь, данная инструкция сэкономит кому-нибудь пару-тройку дней жизни.
Кстати, статья основана на мануале китайского товарища, прочитанном с помощью Google Translate. Спасибо ему, а то биться мне головой об стену гораздо дольше.
Может, напишу как-нибудь об использовании самой библиотеки. Возможности у нее большие, информации крайне мало, код кривой, а мне со всем этим удалось-таки что-то сделать.

Ссылки

18 комментариев:

  1. Дарья, вы - гений! Недавно ставилась задача слияния/конвертирования видео и мы не справились... даже как подступиться не придумали... теперь понятно - как надо было идти!
    P.S. просто любопытно - а вы код будете открывать под GPL, дабы не нарушить лицензию?

    ОтветитьУдалить
  2. Антон, так ведь: --enable-nonfree
    Как я понял, ffmpeg можно собирать и под gpl и нет.

    ОтветитьУдалить
  3. Тут главный момент в том, что ffmpeg распространяется под LGPL. Т.е. библиотеку можно линковать с приложением, распространяемым под любой лицензией

    ОтветитьУдалить
  4. Вставлю свои пять копеек.
    Тоже собирал ffmpeg под Android, но его возможности использовал не через API, а сделал обертку вокруг функции main и далее обращался к собранной либе как из командной строки. Это намного удобнее, т.к. документации по командам ffmpeg'а во много раз больше.

    ОтветитьУдалить
  5. @jeck_landin
    Хитрый ты.

    Кстати, господа, мне тут недавно сообщили, что по данной инструкции ffmpeg собирается только для NDK r4. А так как править ее я буду не раньше, чем снова столкнусь с подобной задачей, прошу иметь в виду и разбираться самостоятельно.

    ОтветитьУдалить
  6. @darja
    Насколько я помню - насчет собирается или нет не знаю, но вот то что уже собрано работало у меня с таким как у вас Abdroid.mk только в NDK 4 версии. Чтобы заработало в 5 вставил такую строку в Android.mk:
    LOCAL_LDLIBS += -llog -lavformat -lavcodec -lavutil -lavcore -L/путь к файлам типа libavcodec.a и тд и тп

    Вобщем теперь у меня работает в любой НДК:) Если что - надеюсь моя строка поможет

    ОтветитьУдалить
  7. Да уж, попытка собрать по инструкции с ndk5 обошлась мне в 3 дня ...

    ОтветитьУдалить
  8. А есть продолжение файлика mylib.c, а то я как явер не знаю С++, а либа эта очень заинтересовала?

    ОтветитьУдалить
  9. Народ, подскажите новичку. В сях я совсем не силен, но придется, похоже, разбираться с ffmpeg для Android.
    Мне не понятен такой момент. Если библиотека компилируется для определенной архитектуры процессоров, то как насчет совместимости? Смартфоны то на разных процах постороены...

    ОтветитьУдалить
  10. а где
    env->ReleaseStringUTFChars(filename, str);
    Вы пробовали запись аудио на OpenAl?
    (не удалось alcCaptureOpenDevice с NULL именем)

    ОтветитьУдалить
  11. Добавлю. Для того что бы оно собралось по ndk-r5 надо в файл /jni/ffmpeg/av.mk в флаги добавить -std=c99

    полсе этого сам ffmpeg соберется. Далее все получившиеся *.a файлы складываем в папку jni/lib нашего проекта где мы будем использовать ffmpeg и изменяем приведенный в статье Anroid.mk на следующий

    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)

    LOCAL_MODULE := libavformat
    LOCAL_SRC_FILES := lib/libavformat.a

    include $(PREBUILT_STATIC_LIBRARY)

    include $(CLEAR_VARS)

    LOCAL_MODULE := libavcodec
    LOCAL_SRC_FILES := lib/libavcodec.a

    include $(PREBUILT_STATIC_LIBRARY)

    include $(CLEAR_VARS)

    LOCAL_MODULE := libpostproc
    LOCAL_SRC_FILES := lib/libpostproc.a

    include $(PREBUILT_STATIC_LIBRARY)

    include $(CLEAR_VARS)

    LOCAL_MODULE := libswscale
    LOCAL_SRC_FILES := lib/libswscale.a

    include $(PREBUILT_STATIC_LIBRARY)

    include $(CLEAR_VARS)

    LOCAL_MODULE := libavutil
    LOCAL_SRC_FILES := lib/libavutil.a

    include $(PREBUILT_STATIC_LIBRARY)

    include $(CLEAR_VARS)

    LOCAL_LDLIBS += -llog
    LOCAL_STATIC_LIBRARIES := libavformat libavcodec libpostproc libswscale libavutil
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/ffmpeg
    LOCAL_SRC_FILES := mylib.c
    LOCAL_MODULE := mylib

    include $(BUILD_SHARED_LIBRARY)

    LOCAL_PATH := $(call my-dir)
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/ffmpeg
    include $(all-subdir-makefiles)

    надеюсь кому-нибудь помог

    ОтветитьУдалить
  12. А у кого-нибудь собралось в NDK без опции:
    --disable-asm

    Дело в том, что эта опция убирает все ARM-оптимизации процессора и код работает в разы медленнее, как минимум в 2 раза медленнее.

    У меня, если убирается эта опция не компилится под NDK. Вываливается ошибка что такая-то ассемблерная команда не поддерживается выбранным процессором.

    Есть какие-нибудь идеи как этого избежать?

    ОтветитьУдалить
  13. Разобрался, cflags нужно продублировать в av.mk, тогда компилится нормально. С аппаратным ускорением, кстати, работает раз 5-6 быстрее чем в примере с опцией --disable-asm

    ОтветитьУдалить
  14. Dima L. выложите свой конфиг с --disable-asm пожалуйста, никак не получается заставить компилироваться ffmpeg с этим параметром.

    ОтветитьУдалить
  15. Добрый день.
    Дарья, спасибо. По вашей статье таки собрал ffmpeg под android.

    Но у меня есть вопрос:
    jeck_landin пишет что написал обертку и теперь использует ffmpeg как из командной строки. Кто нибудь подскажет как это можно сделать? Что то я не разбирусь :(
    А то перекопал кучу инфы но с API разбираться тяжело... я далек от СИ.

    ОтветитьУдалить
  16. У меня благодаря Дарье получилось собрать FFMPEG в таком виде, для NDK r5 пришлось ещё пошаманить.

    Для сборки с ARM-оптимизациями очень долго плясал с бубном, переделывая config.mak и config.h. Потом для включения ARM-версий исходников для libavcodec потребовалось кусок arm/Makefile переносить на верхний уровень.

    Но когда всё собралось и слинковалось, ARM-оптимизация не очень помогла. avcodec_decode_video2() не ускорился и в 2 раза, sws_scale() для YUV->RGB как выполнялся 20 мс для VGA-кадра, так и продолжает (впрочем, в libswscale нет ARM-ветки кода, что странно). А ведь я ещё добавил опцию -ftree-vectorize.

    ОтветитьУдалить
  17. Браво! Не "хило" так, как для убежденного адепта Майкрософта!

    ОтветитьУдалить
  18. Может кому будет интересно в конфигурационном скрипте:
    --extra-ldflags="-Wl,-T,$PREBUILT/arm-eabi/lib/ldscripts/armelf.x -Wl,-rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -nostdlib $PREBUILT/lib/gcc/arm-eabi/4.4.0/crtbegin.o $PREBUILT/lib/gcc/arm-eabi/4.4.0/crtend.o -lc -lm -ldl"
    Вся "лабуда" нивелируется аргументом -nostdlib, поэтому достаточно только указать эту опцию.
    Но это не совсем хороший подход - юзать nostdlib.

    ОтветитьУдалить

Пожалуйста, пишите содержательные комментарии и соблюдайте вежливость.