Написание приложений для Android, связанных с записью и обработкой видео, — довольно сложная задача. Использование стандартных средств, таких как MediaRecorder, не представляет особой сложности, но если пытаться делать что-нибудь выходящие за рамки обычного — начинается настоящее “веселье”.

Что не так с видео на Android


Функционал для работы с видео в Android до версии 4.3 весьма скудный: есть возможность записать видео с камеры с помощью Camera и MediaRecorder, применить стандартные цветовые фильтры камеры (сепия, черно-белый и т.п.) и это, пожалуй, все.

Начиная с версии 4.1 появилась возможность использовать класс MediaCodec, который дает доступ к низкоуровневым кодекам и класс MediaExtractor, который позволяет извлекать закодированные медиа-данные из источника.

Схема


В Android 4.3 появился класс MediaMuxer, который может осуществлять запись нескольких видео- и аудио-потоков в один файл.

Схема


Здесь уже у нас появляется больше возможностей для творчества: функционал позволяет не только кодировать и декодировать видео потоки, но и производить некоторую обработку видео при записи.

В проекте, над которым я работал, стояло несколько требований к приложению:

  • Запись нескольких чанков видео общей продолжительностью до 15 секунд
  • «Склеивание» записанных чанков в один файл
  • “Fast Motion” — эффект ускоренной съемки (time-lapse)
  • “Slow Motion” — эффект замедленной съемки
  • “Stop Motion” — запись очень коротких видео (состоящих из пары-тройки фреймов), практически фотография в формате видео
  • Кадрирование видео и накладывание водяного знака (watermark) для загрузки в соц. сети
  • Накладывание музыки на видео
  • Реверсивное видео

Инструменты


Изначально запись видео производилась с помощью MediaRecorder’a. Этот способ самый простой, давно используется, имеет много примеров и поддерживается всеми версиями Android. Но он не поддается кастомизации. Кроме того, стартует запись при использовании MediaRecorder’a с задержкой около 700 миллисекунд. Для записи маленьких кусочков видео почти секундная задержка неприемлема.

Поэтому было решено увеличить минимально совместимую версию Android 4.3 и использовать MediaCodec и MediaMuxer для записи видео. Это решение позволило избавиться от задержки при инициализации записи. Для рендеринга и модификации захваченных с камеры фреймов был использован OpenGL в связке с шейдерами.

За основу были взяты примеры от Google. Проект называется Grafika и представляет собой компиляцию из костылей примеров, которые могут помочь разобраться с использованием средств для записи и обработки видео.
Для пост-обработки видео был использован FFmpeg. Основная трудность с ffmpeg — сборка нужных модулей и подключение к своему проекту. Это долгий процесс требующий определенных навыков, поэтому мы использовали уже готовую сборку под Android. Особенность работы с большинством подобных сборок ffmpeg такова, что его необходимо использовать как исполняемый файл командной строки: передать строковую команду с входными параметрами и параметрами, которые должны быть применены к итоговому видео. Отсутствие возможности дебага, да и вообще узнать в чем ошибка, если что-то пошло не так, тоже сильно удручает. Единственный источник информации — файл лога, который записывается во время работы ffmpeg. Поэтому, по началу, много времени уходит на то, чтобы разобраться как работает та или иная команда, как делать составные команды, которые будут выполнять несколько действий сразу и т.п.

Slow Motion


От реализации Slow Motion на данный момент отказались, потому что аппаратной поддержки записи видео с достаточной частотой кадров у подавляющего большинства Android устройств нет. Так же нет нормальной возможности “активировать” эту функцию даже на той малой доли устройств, на которых аппаратная поддержка есть.

Можно сделать программный слоу-мо, для этого есть варианты:

  • Дублировать кадры при записи, либо продлевать их продолжительность (время, которое фрейм показывается).
  • Записывать видео, а затем обрабатывать — опять же — дублируя или продлевая каждый кадр.

Но результат получается довольно низкого качества:



Fast Motion


Зато с записью time-lapse видео проблем не возникает. При записи с помощью MediaRecorder’а можно задать частоту кадров, допустим, 10 (стандартная частота кадров для записи видео — 30), и записываться будет каждый третий кадр. В результате видео будет ускорено в 3 раза.

Код
    private boolean prepareMediaRecorder() {
        if (camera == null) {
            return false;
        }
        camera.unlock();
        if (mediaRecorder == null) {
            mediaRecorder = new MediaRecorder();
            mediaRecorder.setCamera(camera);
        }

        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

        CamcorderProfile profile = getCamcorderProfile(cameraId);
        mediaRecorder.setCaptureRate(10);        // Здесь мы задаем частоту кадров при записи видео
        mediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
        mediaRecorder.setVideoFrameRate(30);
        mediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
        mediaRecorder.setOutputFile(createVideoFile().getPath());
        mediaRecorder.setPreviewDisplay(cameraPreview.getHolder().getSurface());
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);

        try {
            mediaRecorder.prepare();
        } catch (Exception e) {
            releaseMediaRecorder();
            return false;
        }

        return true;
    }




Stop Motion


Для мгновенной записи нескольких фреймов стандартный вариант с MediaRecorder не подходит из-за долгой задержки перед стартом записи. Но использование MediaCodec и MediaMuxer решает проблему с производительностью.



Склеивание записанных кусков в один файл


Это одна из основных фич приложения. В результате, после записи нескольких чанков, пользователь должен получить один цельный видео-файл.

Изначально, для этого использовался ffmpeg, но пришлось отказаться от этой затеи, поскольку ffmpeg склеивал видео с транскодированием, и процесс получался достаточно долгим (на Nexus 5, склеиване 7-8 чанков в одно 15-ти секундное видео занимало больше 15 секунд, а для 100 чанков время увеличивалось до минуты и более). Если же использовать более высокий битрейт или кодеки, которые при том же битрейте выдают результат лучше, то процесс занимал еще больше времени.

Поэтому сейчас используется библиотека mp4parser, которая, по-сути, вытаскивает из файлов-контейнеров энкодированные данные, создает новый контейнер, и складывает данные друг за другом в новый контейнер. Потом записывает информацию в хидеры контейнера и все, на выходе получаем цельное видео. Единственное ограничение в этом подходе: все чанки должны быть энкодированы с одинаковыми параметрами (тип кодека, разрешение, соотношение сторон и т.п.). Этот подход отрабатывет за 1-4 секунды в зависимости от количества чанков.

Пример использования с mp4parser’a для склеивания нескольких видео файлов в один
public void merge(List<File> parts, File outFile) {
  try {
    Movie finalMovie = new Movie();
    Track[] tracks = new Track[parts.size()];
    for (int i = 0; i < parts.size(); i++) {
      Movie movie = MovieCreator.build(parts.get(i).getPath());
      tracks[i] = movie.getTracks().get(0);
    }
    finalMovie.addTrack(new AppendTrack(tracks));
    FileOutputStream fos = new FileOutputStream(outFile);
    BasicContainer container = (BasicContainer) new DefaultMp4Builder().build(finalMovie);
    container.writeContainer(fos.getChannel());
  } catch (IOException e) {
    Log.e(TAG, "Merge failed", e);
  }
}


Наложение музыки на видео, кадрирование видео и накладывание водяного знака


Здесь уже не обойтись ffmpeg. Для примера, вот команда, которая накладывает на видео звуковую дорожку:

ffmpeg -y -ss 00:00:00.00 -t 00:00:02.88 -i input.mp4 -ss 00:00:00.00 -t 00:00:02.88 -i tune.mp3 -map 0:v:0 -map 1:a:0 -vcodec copy -r 30 -b:v 2100k -acodec aac -strict experimental -b:a 48k -ar 44100 output.mp4

-ss 00:00:00.00 — время с которого нужно начать обработку в данном случае
-t 00:00:02.88 — время по которое нужно продолжать обработку входного файла
-i input.mp4 — входной видео-файл
-i tune.mp3 — входной аудио-файл
-map — мапинг видео-канала и аудио-канала
-vcodec — установка видео-кодека (в данном случае используется тот же кодек, которым энкодировано видео)
-r — установка фрейм-рейта
-b:v — установка битрейта для видео-канала
-acodec — установка аудио-кодека (в данном случае мы использует AAC кодирование)
-ar — семпл-рейт аудио-канала
-b:a — битрейт аудио-канала

Команда, для наложения вотермарки и кадрирования видео:

ffmpeg -y -i input.mp4 -strict experimental -r 30 -vf movie=watermark.png, scale=1280*0.1094:720*0.1028 [watermark]; [in][watermark] overlay=main_w-overlay_w:main_h-overlay_h, crop=in_w:in_w:0:in_h*in_h/2 [out] -b:v 2100k -vcodec mpeg4 -acodec copy output.mp4

movie=watermark.png — задаем путь к вотермарке
scale=1280*0.1094:720*0.1028 — указываем размер
[in][watermark] overlay=main_w-overlay_w:main_h-overlay_h, crop=in_w:in_w:0:in_h*in_h/2 [out] — накладываем вотермарку и обрезаем видео.

Реверсивное видео


Для создания реверсивного видео нужно совершить несколько манипуляций:

  • Извлечь из видео-файла все фреймы, записать их на внутреннее хранилище (например, в jpg файлы)
  • Переименовать фреймы, чтобы они располагались в обратном порядке
  • Собрать из файлов видео

Решение не выглядит элегантным или производительным, но альтернатив особо нет.

Пример команды для разбивки видео на файлы с кадрами:

ffmpeg -y -i input.mp4 -strict experimental -r 30 -qscale 1 -f image2 -vcodec mjpeg %03d.jpg

После этого нужно переименовать файлы кадров так чтобы они были в реверсивном порядке (т.е. первый кадр станет последним, последний — первым; второй кадр — предпоследним, предпоследний — вторым и т.д)

Затем, с помощью следующей команды можно собрать видео из кадров:

ffmpeg -y -f image2 -i %03d.jpg -r 30 -vcodec mpeg4 -b:v 2100k output.mp4



Видео-гифка


Так же, один из функционалов приложения — создание коротких видео, состоящих из нескольких кадров, что при зацикливании создает эффект гифки. Эта тема сейчас пользуется спросом: Instagram даже недавно запустили Boomerang — специальное приложение для создания таких «гифок».

Процесс довольно прост — делаем 8 фотографий с равным промежутком времени (в нашем случае, 125 миллисекунд), затем дублируем в обратном порядке все кадры, за исключением первого и последнего, чтобы достичь гладкого реверс-эффекта, и собираем кадры в видео.

Например, с помощью ffmpeg:

ffmpeg -y -f image2 -i %02d.jpg -r 15 -filter:v setpts=2.5*PTS -vcodec libx264 output.mp4

-f — формата входящих файлов
-i %02d.jpg — входные файлы с динамическим форматом имени (01.jpg, 02.jpg и т.д.)
-filter:v setpts=2.5*PTS продлеваем продолжительность каждого кадра в 2.5 раза

На данный момент для оптимизации UX (чтобы пользователь не ждал долгой обработки видео) мы создаем сам видео файл уже на этапе сохранения и шаринга видео. До этого работа происходит с фотографиями, которые загружаются в оперативную память и рисуются на Canvas’e TextureView.

Процесс отрисовки
    private long drawGif(long startTime) {
        Canvas canvas = null;
        try {
            if (currentFrame >= gif.getFramesCount()) {
                currentFrame = 0;
            }
            Bitmap bitmap = gif.getFrame(currentFrame++);
            if (bitmap == null) {
                handler.notifyError();
                return startTime;
            }

            destRect(frameRect, bitmap.getWidth(), bitmap.getHeight());

            canvas = lockCanvas();

            canvas.drawBitmap(bitmap, null, frameRect, framePaint);

            handler.notifyFrameAvailable();

            if (showFps) {
                canvas.drawBitmap(overlayBitmap, 0, 0, null);
                frameCounter++;
                if ((System.currentTimeMillis() - startTime) >= 1000) {
                    makeFpsOverlay(String.valueOf(frameCounter) + "fps");
                    frameCounter = 0;
                    startTime = System.currentTimeMillis();
                }
            }
        } catch (Exception e) {
            Timber.e(e, "drawGif failed");
        } finally {
            if (canvas != null) {
                unlockCanvasAndPost(canvas);
            }
        }

        return startTime;
    }   

    public class GifViewThread extends Thread {

        public void run() {
            long startTime = System.currentTimeMillis();
            try {
                if (isPlaying()) {
                    gif.initFrames();
                }
            } catch (Exception e) {
                Timber.e(e, "initFrames failed");
            } finally {
                Timber.d("Loading bitmaps in " + (System.currentTimeMillis() - startTime) + "ms");
            }
            long drawTime = 0;
            while (running) {
                if (paused) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                if (surfaceDone && (System.currentTimeMillis() - drawTime) > FRAME_RATE_BOUND) {
                    startTime = drawGif(startTime);
                    drawTime = System.currentTimeMillis();
                }
            }
        }
    }




Вывод


В целом, работа с видео на платформе Android — та еще боль. Для реализации более-менее продвинутых приложений требуется много времени, костылей нестандартных решений и, скорее всего, углубление в JNI. Хуже всего, что на платформе iOS множество вещей работает “из коробки” или с гораздо меньшим количеством трудозатрат. В планах на будущее хотелось бы сделать свою сборку ffmpeg и использовать ее на уровне JNI, что позволит увеличить производительность, гибкость в использовании, а так же уменьшить общий вес библиотеки (поскольку далеко не все модули нужны в проекте).

Комментарии (9)


  1. Ganster41
    09.12.2015 11:16

    поскольку ffmpeg склеивал видео с транскодированием

    Может вы просто не разобрались как его готовить? Все время клею/режу видео при помощи ffmpeg, именно из-за возможности сделать это без перекодирования.


    1. voltazor
      09.12.2015 13:16

      Все мои попытки не увенчались успехом. Можете что-нибудь посоветовать?


      1. Ganster41
        09.12.2015 15:12

        В ffmpeg обычно одно и то же действие можно сделать даже несколькими способами. Конкретно про конкатенацию файлов есть здесь, например.

        ffmpeg -i "concat:input1.mp4|input2.mp4|input3.mp4" -c copy output.mp4
        


        1. voltazor
          09.12.2015 16:57

          Да, действительно работает и достаточно быстро. Единственное, нужно сначала транскодировать в mpeg, как показано в примере:

          ffmpeg -i input1.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate1.ts
          ffmpeg -i input2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate2.ts
          ffmpeg -i "concat:intermediate1.ts|intermediate2.ts" -c copy -bsf:a aac_adtstoasc output.mp4
          

          Спасибо! :)


  1. sim-dev
    09.12.2015 12:32

    Меня давно беспокоит один вопрос: возможно ли (и если да, то как именно) вести запись видео из одного источника (камеры) одновременно в два разных файла? Хоть бы и без всякой обработки.
    Сам, увы, только собираюсь попробовать начать приступать осваивать андроид…
    Почему волнует? Хочу понять, почему приложения-видеорегистраторы не записывают фрагменты с перекрытием во времени и по возможности решить эту проблему.


    1. withoutuniverse
      09.12.2015 12:50

      А для чего нужно перекрытие? Ни 1 кадра не теряется, смысло мало в этом имхо.

      По вопросу записи сразу в 2 файла — да, почти из коробки.
      Пишете вы в OutputStream, и по факту это может быть обертка под другие File или Memory OutputStream в любом количестве.


      1. sim-dev
        09.12.2015 13:22

        Возможно, я неверно сформулировал проблему… Попробую описать детальнее.
        Все видеорегистраторы пишут видео фрагментами, чтобы по мере заполнения выделенного пространства на флешке перезаписывать (затирать) наиболее старые фрагменты, как наименее значимые. Однако ВСЕ известные на сегодня приложения-видеорегистраторы для андроид не делают перекрытие видеофрагментов ни на секунду, и даже наоборот, начало нового всегда запаздывает (на разное время — видимо, зависит от качества приложения и ресурсов системы, где оно работает). Таким образом возможна ситуация, когда авария произойдет в момент, когда один фрагмент уже закончен, а новый еще не начат, что для видеорегистратора крайне нежелательно. Поэтому все более-менее серьезные [аппаратные] видеорегистраторы пишут фрагменты с регулируемой перекрышей — от 1 до 5 секунд примерно. А вот программные андроид-регистраторы такой перекрыши не делают — я тестировал больше десятка приложений, ни одно так не умеет! Это недомыслие разработчиков или ограничение системы?
        Я так понимаю, что проблема может крыться только в одном: камера, как источник видео, является разделяемым ресурсом или нет? Ранее в J2ME камера была полностью неразделяемым ресурсом, т.е. ее захватить можно было только один раз, иначе говоря, создать объект-камеру можно было только единожды, все остальные попытки приводили к падению приложения (или exception). Т.к. я с андроидом в плане софта не знаком, я и спрашиваю: возможно ли вторично создать объект-камеру и брать из нее видеопоток, направляя его в другой файл, в то время как видеопоток уже куда-то шел?


        1. withoutuniverse
          09.12.2015 15:33

          С проблемой потери кадров я не сталкивался, но описал способ, при котором можно писать в любой файл с любым перекрытием.
          Камера пишет в стрим. То, что он обертка над 10 другими стримами никого не волнует. Она не выключается ине переподключается при создании нового файла, в этом нету смысла.


        1. voltazor
          09.12.2015 17:14

          В целом, проблем быть не должно, если вы будете использовать MediaCodec и MediaMuxer. MediaCodec может работать все время, пока работает камера, а MediaMuxer можно к нему подключать «на лету» или же создавать сразу несколько.
          Что касается камеры — она не разделяемый ресурс.