Говорящая панда или что можно сделать с FFmpeg и OpenCV на Android
Некоторые описываемые вещи наверняка покажутся многим очевидными, тем не менее, для меня, как для разработчика под Windows, эти задачи оказались новыми и их решение было не очевидным, поэтому описание технических деталей я постарался сделать максимально простым и понятным для людей, не имеющих большого опыта работы с Android NDK и всем, что с ним связано. Некоторые решения были найдены интуитивно, и поэтому, скорее всего, они не совсем «красивые».
Предыстория
Идея Android приложения, где бы использовались FFmpeg и OpenCV, появилась после просмотра одного рекламного ролика про минеральную воду Витутас (можете поискать на Youtube). В этом ролике иногда мелькали фото разных животных, которые вращали человеческими глазами, шевелили губами и уговаривали зрителя купить эту воду. Выглядело это довольно забавно. В общем, возникла мысль: что если дать пользователю возможность самому делать подобные ролики, причем не с компьютера, а с помощью своего телефона?
Разумеется, сперва поискали аналоги. Что-то более или менее похожее было найдено в AppStore для iPhone, причем там процесс создания ролика был не очень удачным: выбираешь картинку, размечаешь на ней одну область, и потом камерой в этой области что-то снимаешь, то есть речь о том, чтобы наложить на картинку в разных местах хотя бы два глаза и рот вообще не шла. В Google Play же вообще ничего такого не было. Максимально близкие программы с похожим функционалом были такие, где на фото можно наложить анимированные элементы из ограниченных наборов.
Одним словом, конкурентов мало, поэтому было принято решение приложение всё же делать.
Выбор технологий
Сразу возник логичный вопрос: «А как это все сделать?». Потратив дня два на изучение всяких библиотек для обработки видео и изображений, остановился на FFmpeg и OpenCV.
- обе библиотеки написаны на C/C++, то есть объединить их уже теоретически можно;
- бесплатные с открытым кодом: FFmpeg можно собрать под LGPL, а OpenCV вообще распространяется под лицензией BSD;
- очень быстрые, да и вообще крутые с какой стороны не посмотри.
- написаны на C/C++;
- все же FFmpeg сложноватый для того, чтобы в его коде быстро разобраться и понять, где надо что поменять.
Надо признаться в том, что так как опыта разработки под Android было мало, а под Windows много, то прежде чем начать ковыряться в Eclipse и NDK я сделал маленькую программку в Visual Studio, которая доказала, что сама идея использовать FFmpeg и OpenCV имеет право на жизнь и, самое главное, что есть способ реализовать их взаимодействие. Но о реализации взаимодействия этих библиотек будет написано чуть позже, а это скорее легкая рекомендация на тему того, что лучше все же потратить время и проверить какую-то идею на технологиях, в которых разбираешься лучше всего, чем сразу с головой лезть во что-то новое.
Насчет же компиляции FFmpeg в Visual Studio — сделать это оказалось на удивление легко, но эта статья все же об Android, поэтому если тема FFmpeg в Visual Studio интересна, то напишите об этом, и я постараюсь найти время и написать инструкцию о том, как это сделать.
Итак, проверив, что идея объеденить FFmpeg и OpenCV работает, я приступил к разработке непосредственно Android приложения.
- Компилируем и собираем FFmpeg и OpenCV под Android.
- Пишем код их взаимодействия.
- Используем это все в Java коде приложения.
Делать это все решил в Eclipse, а не в Android Studio — как-то она мне на момент начала разработки показалась сыроватой и не очень удобной.
FFmpeg, Android, NDK, Eclipse, Windows
Первым делом, как все нормальные люди, я стал искать в интернете инструкции о том, как сделать кросс-компиляцию FFmpeg для Android в Windows. Статьи есть, предлагаются даже какие-то наборы make-файлов, есть что-то на гитхабе, но по ним мне не удалось это сделать. Возможно из-за отсутсвия опыта работы с этим всем, возможно из-за ошибок в этих инструкциях и make-файлах. Обычно подобные инструкции пишет человек, который хорошо разбирается в описываемых технологиях и поэтому опускает какие-то «очевидные» нюансы, и получается, что новичку этим невозможно пользоваться.
В общем, пришлось с нуля сделать все самому. Ниже приводится примерная последовательность действий.
0. ПредустановкиСкачиваем и устанавливаем: Eclipse с CDT, Android SDK, NDK, cygwin и OpenCV Android SDK. Если есть необходимость поддержать Android на x86, то следует скачать еще и yasm — он нужен, чтобы сделать кросс-компиляцию *.asm файлов, но об этом позже.
Инструкции по установке и настройке этого всего находятся на сайтах, откуда они собственно и скачиваются, а насчет установки и настройки NDK в Eclipse есть отличная статья на сайте opencv.org, которая выдается в гугле по запросу «OpenCV Introduction into Android Development», обязательно зайдите на нее.
1. Подготавливаем проектСоздаем в Eclipse новый проект Android приложения и ковертируем его в C/C++ проект (смотрите статью «OpenCV Introduction into Android Development»). На самом деле Android проект не сконвертируется полностью в C/C++, а в него просто добавится возможность работать с C/C++.
Скачиваем и распаковываем архив с кодом FFmpeg с сайта ffmpeg.org. Папку с кодом вида «ffmpeg-2.6.1» кидаем в папку «jni» проекта (если ее нет — создаем там же где лежат «res», «scr» и т. п.).
Теперь необходимо создать конфигурационные файлы (самый важный из них «config.h») и make-файлы для FFmpeg. Здесь возникает первый нюанс: существуюет три платформы Android устройств — Arm, x86, MIPS. Для каждой из этих архитектур нужны собрать свои файлы библиотек *.so (аналог *.dll в Windows). NDK позволяет это сделать — в него включены компилятор и линковщик для каждой платформы.
Для того, чтобы сгенерировать конфигурационные файлы в FFmpeg есть специальный скрипт, для запуска которого нам и нужно было установить cygwin. Итак, запускаем командную строку Cygwin Terminal и вводим приведенные ниже наборы команд.
Для устройств ARM:
Для устройств x86:
Для устройств MIPS:
- Первые две команды — переход в папку с кодом FFmpeg, где лежит файл «configure» — это bash скрипт, который генерирует нужные нам файлы, в том числе и «config.h», который используется при компиляции;
- Далее мы создаем три временные переменные окружения PREBUILT, PLATFORM, TMPDIR, в первые две записываются пути к папкам в NDK, где лежат утилиты для кросс-компиляции под разные платформы, если в эти папки зайти, то там будет среди множества папок будет и папка «bin», в окторой и лежат компилятор и линковщик. TMPDIR — путь во временную папку, где скрипт при работе будет хранить временные свои файлы;
- Последняя команда это собственно запуск скрипта «configure» из папки «ffmpeg-2.6.1» с параметрами. Список всех возможных параметров скрипта с пояснениями выводятся по команде «./configure -h». Ниже описание тех параметров, которые мы используем:
- параметр --enable-version3 – говорим скрипту, чтобы он сгенерировал такой конфигурационный файл, чтобы с помощью него можно было скомпилировать библиотеки FFmpeg которы будут под лицензией LGPL 3.0;
- параметры --enable-shared и --disable-static – говорим скрипту, что хотим на выходе получить *.so файлы. Сделав это, то по идее в купе с LGPL мы сможем их линковать к своему коду на который LGPL не будет распространяться;
- параметры --disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-ffserver --disable-network – этими параметрами мы говорим скрипту, что нам не нужно создавать консольные программы (другими словами ffmpeg.exe, ffplay.exe и другие);
- оставшиеся параметры понятны по своим названиям — мы говорим, что хотим сделать кросс-компиляцию, назначаем платформу (linux arm, mips или x86), прописываем скрипту пути к компилятору и линковщику, задаем путь к временной папке.
Скрипт сгенерирует в папке «jni/ffmpeg-2.1.3» файл «config.h», «config.asm» и несколько make-файлов.
2. Make-файлы, компиляция и сборкаИтак, на данном этапе у нас уже есть: проект Android-приложения в Eclipse, в папке «jni/ffmpeg-2.1.3» лежит код с FFmpeg, и только что мы сгенерировали нужный нам файл «config.h». Теперь надо сделать make-файлы, чтобы это все можно было скомпилировать и получить *.so файлы которые мы сможем использовать в Android приложении.
Я пробовал использовать для компиляции make-файлы, сгенерированные скриптом, но у меня не получилось, скорее всего, по причине кривизны рук. Поэтому я решил сделать свои собственные make-файлы с комментариями и инклюдами.
Для компиляции с помощью NDK необходимо минимум два make-файла: Android.mk и Application.mk, которые должны находиться в папке «jni» проекта. Application.mk обычно содержит не более десятка параметров и настраивает, если можно так выразиться, «глобальные» параметры компиляции. Android.mk отвечает за все остальное.
- компилятор ругается на символ «av_restrict»: надо открыть файл «config.h» в папке «ffmpeg-2.1.3» и заменить строку «#define av_restrict restrict» на «#define av_restrict __restrict»;
- на некоторых машинах компилятор ругается на «getenv()»: это решается комментированием строки «extern char *getenv(const char *);» в файле «ndk\android-ndk-r9c\platforms\android-9\arch-arm\usr\include\stdlib.h». Обратите внимание, на путь — в нем есть папка зависимая от целевой архитектуры: «arch-arm» если вы компилируете под для Arm, «arch-mips» для MIPS и «arch-x86» для x86;
- для избежания конфликтов лучше переименовать в файле «ffmpeg.c» функцию main() в ffmpeg_main(). Ее мы будем вызывать из Java кода Android приложения.
- помимо непосредственно модулей библиотеки FFmpeg, таких как libavcodec, libavfilter и так далее, еще нам надо будет скомпилировать код самой программы ffmpeg.exe как библиотеку ffmpeg.so. Мы будем вызывать ее main() функцию (которую мы переименовали в ffmpeg_main()) из Java-кода и передавать туда нужные нам параметры, а она будет делать для нас нужную работу;
- NDK не умеет компилировать *.asm файлы для x86 архитектуры, поэтому нам надо в начале с помощью yasm скомпилировать *.asm файлы, которые лежат в папках вида «jni\ffmpeg-2.1.3\libavcodec\x86», «jni\ffmpeg-2.1.3\libavfilter\x86» и так далее. Ниже будут приведены *.bat файлы, которые это делают. На выходе мы получим объектные файлы *.a, путь к которым мы укажем линковщику при компиляции под x86. Это надо сделать только если вы собираетесь поддерживать эту архитектуру;
- в make-файле практически для каждого модуля включается еще один make-файл вида «libavfilter_ignore.mk». В них для каждого модуля прописаны *.c файлы, которые не надо компилировать. Что это за файлы: часть из них, как я понял из содержимого, что-то вроде тестовых программок (как, например, mpegaudio_tablegen.c). Часть из них не предназначенны для компиляции, а просто подключаются через include (например h264qpel_template.c подключается в файле h264qpel.c). Наверняка в make-файлах, сгенерированных после работы скрипта confgiure (с которыми у меня ничего не сложилось) они все тоже игнорируются;
- в make-файл включена так же и сборка модуля OpenCV. Там все просто, читайте статью на сайте opencv.org под названием «OpenCV Introduction into Android Development». Он нужен для как раз для упомянутого взаимодействия FFmpeg-OpenCV. Об этом взаимодействии будет рассказано ниже;
- все модули FFmpeg с нуля у меня собираются полтора часа. Во время разработки просто невозможно работать, особенно если приходится часто что-то менять в какой-то модуле FFmpeg. Чтобы этого избежать, ниже будет приведен еще один make-файл. В нем с нуля будет компилироваться только один какой-нибудь модуль FFmpeg, а остальные будут использоваться уже ранее скомпилированные. Например, вы работаете в модуле libavfilter, что-то там программируете и постоянно меняете, вам нет нужды постоянно пересобирать еще и libavcodec и другие, достаточно взять уже ранее скомпилированные и нужным образом об этом указать в make-файле;
- в make-файле есть поясняющие комментарии, но так как там компилируется несоклько модулей, причем процедура компиляции похожа, то дублирующиеся команды в make-файле не поясняются.
Эти make-файлы надо положить в папку «jni». В результате структура проекта будет выглядеть примерно как на картинке:
Момент истины. Нажимаем Project Build, и если все хорошо, то через некоторое время (у меня около часа) в папке «libs» проекта должны появиться папки с названиями архитектур и заветные *.so файлы (обратите внимание на файл libopencv_java.so — это файл который был сгенерирован подключенным скриптом OpenCV.mk и файл libffmpeg.so – это файл программы ffmpeg.exe, который мы собрали как *.so файл):
Обязательно сохраните в отдельной папке эти *.so файлы, потом их можно будет повторно использовать в случае когда надо будет изменить код только в одном модуле.
4. Используем FFmpeg модули в Android приложенииЕсли у вас получилось сгенерировать *.so файлы, то пора попробовать их подключить к Android приложению. Можно конечно это сделать в этом же проекте, но будет лучше создать новый. Последовательность действий для этого аналогична созданию проекта, где мы собирали FFmpeg (и не забываем про статью «OpenCV Introduction into Android Development»).
Новый проект принципиально такой же, как и тот, в котором мы собирали FFmpeg файлы: там тоже будет папка jni, в которой будет лежать Android.mk и Application.mk, так же в ней будет лежать файл с кодом на C++ который будет вызывать функции из сгенерированных нами ранее *.so файлов. Но для начала начнем с Java-кода.
Условимся, что у вас есть Activity, на котором есть кнопка, и все действия мы будем делать по нажатии на эту кнопку.
Далее в папке «jni» создаем файл myproject.cpp со следующим содержимым:
Там же создаем файлы Android.mk и Application.mk:
Теперь надо записать на камеру телефона ролик со звуком, и сохранить его как «storage/extSdCard/DCIM/Camera/video.mp4», или пропишите другой путь в myproject.cpp.
Все! Запускайте, нажимайте кнопку, и, если все было сделано правильно, рядом с исходным файлом video.mp4 должен появиться файл video_no_audio.mp4 без аудио потока. Если он появился, то FFmpeg под Android работает!
5. Интегрируем FFmpeg и OpenCVИтак, сейчас мы имеем рабочий FFmpeg и знаем, как его использовать в Java коде Android приложения, осталось каким-то образом прикрутить к FFmpeg OpenCV.
Сразу говорю, ковырялся я в коде FFmpeg довольно долго. Нашел цикл, где декодируются и обрабатываются кадры из видео, изучил его от и до. Прикидывал, где все же вставить код, который вызывал бы функции OpenCV. Под конец совсем уж было отчаялся.
И вдруг меня осенило — так ведь в FFmpeg есть видео фильтры! Фильтры — это модули, которые подключаются на этапе декодирования-кодирования, и им на обработку дается каждый кадр из видео потока. Как раз здесь и можно было бы вставить покадровую обработку видео потока. Сначала хотел написать свой собственный фильтр с конвертацией и OpenCV-вызовами функций. В нем смог бы обрабатывать каждый кадр видео. Но потом решил просто взять какой-нибудь уже готовый фильтр и изменить его код.
Лучше всего мне подошел фильтр масштабирования, который находится в файле «ffmpeg-2.1.3\libavfilter\vf_scale.c», ведь моя задача — формировать кадры для видео на основе какой-нибудь картинки, размер которой может не совпадать с размером кадра видео. Например, пользователь выбрал картинку панды размером 300x300, а видео, где он вращает глазами, снял размером 640x480. Задача программы — наложить глаза из этого видео на картинку панды. Так вот, мысль взять фильтр масштабирования как раз решала эту проблему несоответствия размера кадра исходного видео и размера картинки, которая должна была заменить этот кадр собой.
Я реализовал это, нагло обманув фильтр масштабирования. В аргументах через функцию ffmpeg_process() задавал ему такие параметры, чтобы он думал, что ему надо менять размер кадров видео с 640x480 до 300x300. Но вместо кода масштабирования я вставил код, где из кадра 640x480 вырезаются глаза, накладываются на картинку панды, которая 300x300. И дальше эту картинку панды с глазами записываем в буфер результирующего кадра, и фильтр кодирует этот кадр в видеопоток по всем правилам. Вот здесь как раз и возникает задача взаимодействия OpenCV и FFmpeg.
Решается она так: если вы откроете файл vf_scale.c, то там будет функция filter_frame(). Эта функция и есть тот самый callback, который вызывается при обработке видео для каждого кадра. У нее есть аргумент «in» типа AVFrame* — это и есть декодированый кадр из видео. Нам надо этот кадр преобразовать в формат совместимый с OpenCV и обработать.
Вот и все! Дальше фильтр vf_scale закодирует кадр из _out правильным образом, и на выходе мы получим видеопоток, где вместо кадров исходного видео присутсвует картинка панды с вырезанными элементами из исходного видео (в данном случае два глаза и рот):
Насчет производительности стоит отметить, что несмотря на то, что речь идет о покадровой обработке видео потока, то даже на средних телефонах создание ролика происходит за довольно приемлемое время — все же FFmpeg и OpenCV это очень мощные библиотеки.
Заключение
Я сам имею не очень удачный опыт компиляции FFmpeg по подобным статьям из интернета. Хотя я и хотел подробно все изложить, но вполне возможно, забыл про какие-то нюансы, заранее извиняюсь за это. Если вы попробовали скомпилировать FFmpeg по моей инструкции и где-то застряли — пишите, постараюсь найти время и исправить или дополнить эту статью согласно вашим замечаниям.