воскресенье, 15 апреля 2012 г.

Компиляция ffmpeg для Android-приложения

Привычная для меня среда обитания — Windows. И вдруг оказывается, что компиляция ffmpeg под Android в операционной системе Windows невозможна. Нет, вести разработку с применением откомпилированной библиотеки и заголовочных файлов ffmpeg можно, но чтобы получить эту библиотеку придётся развёртывать Linux. Может я излишне категоричен, и кому-то это удавалось, но я таких примеров не встретил. Наоборот есть много повисших в воздухе вопросов на форумах. Общее направление рекомендаций сводится к переходу на Linux.
Пришлось устанавливать Ubuntu и настраивать в ней среду разработки. Об этом я писал в своей статье «Обустройство в Ubuntu».
Итак, как я компилировал ffmpeg...



Установка Android NDK

Загрузка и установка Android NDK подробно описаны тут. Я использовал Android NDK, Revision 7c (April 2012): android-ndk-r7c-linux-x86.tar.bz2
Внутри архива папка «android-ndk-r7c». Можно просто разархивировать её в Домашнюю папку. Я ещё переименовал её в «android-ndk».

Компиляция ffmpeg

В Eclipse надо cоздать новый Android-проект и добавить в него папку jni.
Называю проект «MyFfmpeg». При создании выбираю «build target» Google APIs 2.3.3 (level 10). Затем:


И Finish.
Далее в проект добавляем папку jni:



Со страницы загрузки ffmpeg я скачал архив стабильного релиза FFmpeg 0.8.11 "Love", разархивировал во временный каталог, переименовал папку «ffmpeg-0.8.11» в «ffmpeg» и прямо drag&drop-ом перенёс её в папку jni моего проекта в Eclipse.
"Love" — самая поздняя версия, которую мне удалось откомпилировать. Я пробовал версии FFmpeg 0.9.1 "Harmony", FFmpeg 0.10 "Freedom" и FFmpeg 0.10.2 "Freedom", но безрезультатно. Получаю ошибку компиляции «error: libavcodec/codec_names.h: No such file or directory». Причём не работает именно компиляция средствами Android NDK. Описание этого бага я нашёл здесь. Решения пока нет (на сегодняшний день уже три месяца). Думаю, эта проблема решаема, но и я не стал сильно упираться — меня вполне устраивает и версия 0.8.11.
Далее файлу …/MyFfmpeg/jni/ffmpeg/configure необходимо установить атрибут разрешения исполнения как программы.
Для конфигурирования ffmpeg и компиляции библиотеки создадим скрипт "build4android.sh" в каталоге «…/MyFfmpeg/jni/ffmpeg». Идею написания такого скрипта я подсмотрел в одном проекте с открытым кодом. Вот текст моего "build4android.sh":

#!/bin/bash
# Author: Dmitry Dzakhov (based on Guo Mingyu's script)

# Creating conf.sh in ffmpeg directory
NDK=/home/dmitrydzz/android-ndk
PLATFORM=$NDK/platforms/android-8/arch-arm
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86
output="conf.sh"

[ -f conf.sh ] && echo "Old $output has been removed."
echo '#!/bin/bash' > $output
echo "PREBUILT=$PREBUILT" >> $output
echo "PLATFORM=$PLATFORM" >> $output
echo './configure --target-os=linux \
    --prefix=./android/armv7-a \
    --enable-cross-compile \
    --extra-libs="-lgcc" \
    --arch=arm \
    --cc=$PREBUILT/bin/arm-linux-androideabi-gcc \
    --cross-prefix=$PREBUILT/bin/arm-linux-androideabi- \
    --nm=$PREBUILT/bin/arm-linux-androideabi-nm \
    --sysroot=$PLATFORM \
    --extra-cflags=" -O3 -fpic -DANDROID -DHAVE_SYS_UIO_H=1 -Dipv6mr_interface=ipv6mr_ifindex -fasm -Wno-psabi -fno-short-enums -fno-strict-aliasing -finline-limit=300 -mfloat-abi=softfp -mfpu=neon -marm -march=armv7-a -mtune=cortex-a8 " \
    --disable-shared \
    --enable-static \
    --extra-ldflags="-Wl,-rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -nostdlib -lc -lm -ldl -llog" \
    --enable-parsers \
    --enable-encoders \
    --enable-decoders \
    --enable-muxers \
    --enable-demuxers \
    --enable-swscale \
    --enable-swscale-alpha \
    --disable-ffplay \
    --disable-ffprobe \
    --enable-ffserver \
    --enable-network \
    --enable-indevs \
    --disable-bsfs \
    --enable-filters \
    --enable-avfilter \
    --enable-protocols \
    --disable-asm \
    --enable-neon' >> $output

# start configure
sudo chmod +x $output
echo "configuring..."
./$output || (echo configure failed && exit 1)

# modify the config.h
echo "modifying the config.h..."
sed -i "s/#define restrict restrict/#define restrict/g" config.h

# remove static functions in libavutil/libm.h
echo "removing static functions in libavutil/libm.h..."
sed -i "/static/,/}/d" libavutil/libm.h

# modify Makefiles
echo "modifying Makefiles..."
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libavcodec/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libavcodec/Makefile
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libavfilter/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libavfilter/Makefile
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libavformat/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libavformat/Makefile
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libavutil/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libavutil/Makefile
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libpostproc/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libpostproc/Makefile
sed -i "/include \$(SUBDIR)..\/subdir.mak/d" libswscale/Makefile
sed -i "/include \$(SUBDIR)..\/config.mak/d" libswscale/Makefile

# generate av.mk in ffmpeg
echo "generating av.mk in ffmpeg..."
echo '# LOCAL_PATH is one of libavutil, libavcodec, libavformat, or libswscale

#include $(LOCAL_PATH)/../config-$(TARGET_ARCH).mak
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))' > av.mk

echo 'include $(all-subdir-makefiles)' > ../Android.mk

echo 'LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_STATIC_LIBRARIES := libavformat libavcodec libavutil libpostproc libswscale
LOCAL_MODULE := ffmpeg
include $(BUILD_SHARED_LIBRARY)
include $(call all-makefiles-under,$(LOCAL_PATH))' > Android.mk

echo '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)' > libavformat/Android.mk

echo '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 += -std=c99
LOCAL_LDLIBS := -lz
LOCAL_STATIC_LIBRARIES := $(FFLIBS)
LOCAL_MODULE := $(FFNAME)
include $(BUILD_STATIC_LIBRARY)' > libavcodec/Android.mk

echo '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)' > libavfilter/Android.mk

echo '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)' > libavutil/Android.mk

echo '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)' > libpostproc/Android.mk

echo '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)' > libswscale/Android.mk

# start build!
echo "start ndk-building..."
cd ../..
$NDK/ndk-build
# Change previous line to "$NDK/ndk-build V=1" if you'll get errors. This will give some more information.

Не забываем установить атрибут исполнения для этого скрипта!
Запускаю терминал и перехожу в каталог …/MyFfmpeg/jni/ffmpeg. Выполняю команду:

./build4android.sh 

Sudo запросит пароль пользователя, а дальше жду. Долго. На моём ноутбуке компиляция заняла 13 минут.
В процессе компиляции будет довольно много предупреждений, но с ними надо смириться. Это вопрос к разработчикам ffmpeg, как я понимаю.

Демонстрация работы

Итак, на предыдущем шаге Android NDK откомпилировал нам библиотеку, в результате чего в нашем проекте MyFfmpeg появились папки …/MyFfmpeg/libs и …/MyFfmpeg/obj. Последняя пригодится в дальнейшем. Для демонстрации использования откомпилированной под Android библиотеки, создадим отдельный проект, в котором будем вызывать функции этой библиотеки.
Говорят, желающие могут на этом шаге вернуться в Windows. Я желающий, но пока остался в Ubuntu.
Новый проект пусть будет называться MyFfmpegTest. После его создания выполняем следующие действия:

1. Копируем в него папку obj из проекта MyFfmpeg.
2. Создаём папку …/MyFfmpegTest/jni. Здесь будет наш код-обёртка, написанный на C.
3. Копируем в папку jni заголовочные файлы ffmpeg. Точнее, я скопировал в jni всю папку …/MyFfmpeg/jni/ffmpeg, а затем почистил её, оставив только файлы с расширением h, S и asm. Для этого в терминале выполнил такую команду:
find ~/robot-mitya/Android/MyFfmpegTest/jni/ffmpeg/ -type f -not \( -name "*.asm" -or -name "*.h" -or -name "*.S" \) -exec rm {} \;
ОСТОРОЖНО: команда удаляет все файлы кроме *.asm, *.h и *.S во всех подкаталогах пути, указанного первым параметром.


4. В папке jni создаём файл 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_ru_dzakhov_ffmpeg_test_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;
}

5. Там же, в jni создаём файл Android.mk:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_LDFLAGS := -Wl,-rpath-link=/home/dmitrydzz/android-ndk/platforms/android-14/arch-arm/usr/lib/ -rpath-link=/home/dmitrydzz/android-ndk/platforms/android-14/arch-arm/usr/lib/
LOCAL_MODULE := libavformat
LOCAL_SRC_FILES := ../obj/local/armeabi/libavformat.a
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon #надо именно эти 3 параметра! Нашёл это в android-ndk/docs/STANDALONE-TOOLCHAIN.html
LOCAL_LDLIBS := -lz -lm -llog -lc -L$(call host-path, $(LOCAL_PATH))/$(TARGET_ARCH_ABI) -landprof

include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_LDFLAGS := -Wl,-rpath-link=/home/dmitrydzz/android-ndk/platforms/android-14/arch-arm/usr/lib/ -rpath-link=/home/dmitrydzz/android-ndk/platforms/android-14/arch-arm/usr/lib/
LOCAL_MODULE := libavcodec
LOCAL_SRC_FILES := ../obj/local/armeabi/libavcodec.a
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon
LOCAL_LDLIBS := -lz -lm -llog -lc -L$(call host-path, $(LOCAL_PATH))/$(TARGET_ARCH_ABI) -landprof

include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)

LOCAL_MODULE := libpostproc
LOCAL_SRC_FILES := ../obj/local/armeabi/libpostproc.a
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon

include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := libswscale
LOCAL_SRC_FILES := ../obj/local/armeabi/libswscale.a
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon

include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := ../obj/local/armeabi/libavutil.a
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon

include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_LDLIBS += -llog -lz
LOCAL_STATIC_LIBRARIES := libavformat libavcodec libpostproc libswscale libavutil
LOCAL_C_INCLUDES += $(LOCAL_PATH)/ffmpeg
LOCAL_SRC_FILES := mylib.c
LOCAL_CFLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon
LOCAL_MODULE := mylib

include $(BUILD_SHARED_LIBRARY)

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

6. Компилируем динамическую библиотеку libmylib.so с помощью Android NDK. Для этого запускаем терминал, переходим в папку …/MyFfmpegTest/jni и выполняем команду:
~/android-ndk/ndk-build.


Библиотека скомпилирована, для демонстрации осталось немного подкрутить активити нашего Android-приложения. Добавляем несколько строк в активити:


package ru.dzakhov.ffmpeg.test;

import android.app.Activity;
import android.os.Bundle;

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/VIDEO0050.mp4");
    }

    static
    {
        System.loadLibrary("mylib");
    }
}

Файл, естественно, укажите какой-нибудь свой. Запускал приложение я сразу на телефоне. В окне LogCat (тег "mylib") проскочили две замечательные строчки, после чего я стал светиться как медный таз:

04-08 19:20:09.999: I/mylib(26025): File was opened
04-08 19:20:09.999: I/mylib(26025): File '/sdcard/VIDEO0050.mp4', Codec mov,mp4,m4a,3gp,3g2,mj2

Путь к этим двум строчкам дался мне с трудом.

Заключение

Сейчас всё выглядит не так уж и сложно, но на компиляцию ffmpeg я потратил уйму сил и времени: пришлось развёртывать невиданную мной ранее Ubuntu, осмысливать Android NDK, искать, искать и искать в интернете крохи информации о компиляции ffmpeg под Android.
В ходе своего поиска пути решения задачи передачи видео от Android к компьютеру, я пытался найти и альтернативные варианты. Они есть и я их рассматривал:

  1. Есть libav. Это «форк» от ffmpeg. Есть мнение, что он лучше, но информации по нему ещё меньше, чем по ffmpeg.
  2. Я встречал проекты под Android с открытым исходным кодом, реализующие передачу видео и аудио по протоколу SIP. Самый известный — Sipdroid. Он даже самостоятельно реализует протокол RTP. Я, конечно, заглянул в исходники и сразу вернулся к идее об ffmpeg.
  3. Я нашёл платный SDK под Android, предназначенный для работы с видео, аудио и SIP. Но он использует ffmpeg. Причём старый — я смотрел исходный код. Стоимость я не выяснил, документацию не нашёл, но по крайней мере, есть у кого её запросить.
  4. Откомпилировать исполняемый файл ffmpeg, а не библиотеку, и работать с ним из моего приложения. Тем более, что работа с ffmpeg из командной строки описана в интернете вполне сносно. Решение какое-то корявое, мне кажется.
  5. Использовать бесплатное приложение IP Webcam. Прекрасный вариант, но уж больно мне хотелось реализовать всё полностью в моём приложении (ffmpeg не в счёт). Работа с ffmpeg это полезный опыт, который пригодится мне в будущем. Тем более, посмотрите, сколько тем пришлось уже для этого поднять.
  6. И самый запасной вариант. Если у меня не получится передача видео, я вполне могу организовать передачу изображений. По моим расчётам, на 5 кадров в секунду вполне можно рассчитывать. Но очень уж хочется потокового видео.
В результате, всё-таки я остался на варианте с ffmpeg, встроенным в мой проект.

На всякий случай вот ссылки на скрипт build4android.sh конфигурирования и компиляции ffmpeg и тестовый проект MyFfmpegTest.

И последнее, что я обязан сказать... Огромное спасибо Дарье Ряжских за её статью «Собираем ffmpeg для Android» и Guo Mingyu (за прекрасную идею и прообраз моего скрипта конфигурирования и компиляции ffmpeg вместо кучи ручных операций), хоть эти люди меня и не знают. Мой труд сводился только к «осовремениваю» их работы и расширению конфигурации ffmpeg. Но поверьте, мне этого более чем хватило.