[Из песочницы] Запись и обработка видео на Android

e608a49e1ebd4d95b28c87fd1cb1e153.jpg

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

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


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

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

Схема
5193907c46d1431e86e04818359aac89.jpg


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

Схема
c5a43ad5155e46c6aac634ea7d8b5688.jpg


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

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

  • Запись нескольких чанков видео общей продолжительностью до 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 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, что позволит увеличить производительность, гибкость в использовании, а так же уменьшить общий вес библиотеки (поскольку далеко не все модули нужны в проекте).

© Habrahabr.ru