Pull to refresh

MediaCodec или понимаем как хотим

Reading time6 min
Views27K
С выходом Android 4.3 (API 18), Google привнесла долгожданный компонент под названием MediaCodec. Класс был открыт публике с выходом API 16, но для нормального использования и поддержки в Android системе требуется минимальный уровень API 18.

Материал рассчитан на опытного Android разработчика. Я попробую объяснить и показать примеры кодирования видео на лету с использованием Surface как входа и выхода потока данных. Если интересно, прошу под кат.

Что такое MediaCodec?


Официальная документация гласит:
MediaCodec class can be used to access low-level media codec, i.e. encoder/decoder components.
или
MediaCodec класс может быть использован для доступа к низко-уровневому медиа кодеку, т.е. кодеру/декодеру
В принципе это кодер или декодер который манипулирует буферами данных. Если мы будем смотреть на формат H264, он же video/avc, то по сути буфер будет хранить NAL кадры и т.д.

Зачем нужен MediaCodec?


В большинстве случаев Android разработчики будут использовать VideoView, MediaPlayer с SurfaceView и этого вполне достаточно. Но как только речь зайдет о создании видео потока для дальнейшей передачи куда-либо, у нас не так много вариантов. Тут нам поможет MediaCodec.

Как кодировать?


MediaCodec дает возможность создать Surface объект для принятия данных для кодера. Я разделил логику кодирвоания на три этапа: подготовка, цикл кодирования, освобождение ресурсов.

Так как процесс кодирования по сути простой цикл обработки буферов данных, нам потребуется создать отдельный поток, который будет представлять непосредственно видео кодер.

class Worker extends Thread {

  // ...

  BufferInfo mBufferInfo; // хранит информацию о текущем буфере
  MediaCodec mEncoder; // кодер
  Surface mSurface; // Surface как вход данных для кодера
  volatile boolean mRunning; 
  final long mTimeoutUsec; // блокировка в ожидании доступного буфера

  public Worker() {
        mBufferInfo = new BufferInfo();
        mTimeoutUsec = 10000l;
   }

  public void setRunning(boolean running) {
     mRunning = running;
  }

   @Override
   public void run() {
      prepare();
      try {
         while (mRunning) {
           encode();
         }
       } finally {
         release();
       }
   }

  // ...

}

Подготовка


Прежде чем приступить к кодированию нам потребуется описать формат и конфигурацию видео на выходе, создать сам кодер и получить Surface объект для ввода данных кодеру.

void prepare() {
        int width = 1280; // ширина видео
        int height = 720; // высота видео
        int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета
        int videoBitrate = 3000000; // битрейт видео в bps (бит в секунду)
        int videoFramePerSecond = 30; // FPS
        int iframeInterval = 2; // I-Frame интервал в секундах

        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);

        mEncoder = MediaCodec.createEncoderByType("video/avc"); // H264 кодек
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер
        mSurface = mEncoder.createInputSurface(); // получаем Surface кодера
        mEncoder.start(); // запускаем кодер
}

Цикл кодирования


void encode() {
        if (!mRunning) {
            mEncoder.signalEndOfInputStream(); // сообщить кодеку о конце потока данных
        }
        // получаем массив буферов кодека
        ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers();
        for (;;) {
            // статус является кодом возврата или же, если 0 и позитивное число, индексом буфера в массиве
            int status = mEncoder.dequeueOutputBuffer(mBufferInfo, mTimeoutUsec);
            if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // нет доступного буфера, пробуем позже
                if (!mRunning) break; // выходим если поток закончен
            } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // на случай если кодек меняет буфера
                outputBuffers = mEncoder.getOutputBuffers();
            } else if (status < 0) {
                // просто ничего не делаем
            } else {
                // статус является индексом буфера кодированных данных
                ByteBuffer data = outputBuffers[status];
                data.position(mBufferInfo.offset);
                data.limit(mBufferInfo.offset + mBufferInfo.size);
                // ограничиваем кодированные данные
                // делаем что-то с данными...
                mEncoder.releaseOutputBuffer(status, false);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                    break;
                }
            }
        }
    }

mBufferInfo и data хранят данные о кадре(ах) который по сути может быть передан по сети или записаны в файл и др. Эти же данные могут быть декодированы h264 кодеком, чтобы получить изображение.

Важно заметить, что BufferInfo класс имеет поле flags. По сути может содержать три флага: BUFFER_FLAG_CODEC_CONFIG, BUFFER_FLAG_END_OF_STREAM и BUFFER_FLAG_SYNC_FRAME.

Давайте рассмотрим каждый из флагов:

  • BUFFER_FLAG_CODEC_CONFIG флаг сигнализирует о том, что буфер данных содержит конфигурацию кодека. Первый доступный буфер кодера должен содержать данный флаг. К примеру для нашего H264 это может быть csd-0, который может быть использован во время декодирования при создании видео формата кодека.
  • BUFFER_FLAG_END_OF_STREAM флаг сигнализирует о том, что достигнут конец потока данных. Я не уверен на счет использования данного флага во время кодирования, т.к. мы контролируем поток данных.
  • BUFFER_FLAG_SYNC_FRAME флаг сигнализирует о том, что кадр является I-Frame. Важно использовать при передачи в файл или по сети, к примеру по протоколу RTP.

Освобождение ресурсов


Тут все просто, останавливаем кодер и освобождаем системные ресурсы.

void release() {
        mEncoder.stop();
        mEncoder.release();
        mSurface.release();
}

Что дальше?


Имея Surface возможны как минимум три пути ввода данных: MediaPlayer (Camera?), OpenGL, Canvas. По примеру использования рендера в SurfaceView возможно создать подобный цикл на основе нашего Surface:

class Renderer extends Thread {

        volatile boolean mRunning;

        public void setRunning(boolean running) {
            mRunning = running;
        }

        boolean draw() {
            Canvas canvas = mSurface.lockCanvas(null);
            if (canvas != null) {
                try {
                    draw(canvas); // рисуем на нашем канвасе
                    return true;
                } finally {
                    if (mSurface.isValid() && mRunning) {
                        mSurface.unlockCanvasAndPost(canvas);
                    }
                }
            }
            return false;
        }

        @Override
        public void run() {
            while (mRunning && draw()) {
            }
        }
}

Вариантов использования может быть несколько. Еще интересный момент, Surface является Parcelable, что дает возможность передачи через Binder в другие процессы, таким образом ваш кодер может находиться в одном процессе, когда рендер в другом.

Как декодировать?


Процесс декодирования схожий на процесс кодирования. Мы так же манипулируем буферами данных. Данный процесс я разделил на четыре части: конфигурация, декодирование сэмпла, цикл декодирования, освобождение ресурсов.

Я опущу детали создания класса потока, архитектура схожа на ту, которую я использовал в процессе кодирования.

Конфигурация


Процесс конфигурации критичен, иначе кодек невозможно создать и использовать для декодирования поступающих данных.

synchronized void configure(Surface surface, int width, int height,
        ByteBuffer csd0) {
    if (mConfigured) { // просто флаг, чтобы знать, что декодер готов
        throw new IllegalStateException();
    }
    // создаем видео формат
    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
    // передаем наш csd-0
    format.setByteBuffer("csd-0", csd0);
    // создаем декодер
    mDecoder = MediaCodec.createDecoderByType("video/avc");
    // конфигурируем декодер
    mDecoder.configure(format, surface, null, 0);
    mDecoder.start();
    mConfigured = true;
}

Важно заметить, мы обязаны знать на какой Surface нужно выводить изображение, так же ширину и высоту изображения. csd-0 так же важен на этом этапе, как было описанно ранее в процессе кодирования, первый буфер данных должен иметь флаг BUFFER_FLAG_CODEC_CONFIG, этот буфер и является csd-0 который необходимо передать на этапе конфигурации декодера.

Декодирование сэмпла


Как только кодированные данные доступны их необходимо передавать декодеру. Мы запрашиваем буфер декодера, как только он доступен, передаем наши данные.

void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) {
    if (mConfigured) {
        // вызов блокирующий
        int index = mDecoder.dequeueInputBuffer(mTimeoutUs);
        if (index >= 0) {
            ByteBuffer buffer = mDecoder.getInputBuffers()[index];
            buffer.clear(); // обязательно сбросить позицию и размер буфера
            buffer.put(data, offset, size);
            // сообщаем системе о доступности буфера данных
            mDecoder.queueInputBuffer(index, 0, size, presentationTimeUs, flags);
        }
    }
}

Если данные были переданы по сети, к примеру RTP протокол, необходимо учесть, что сэмплы передаются последовательно, иначе возможны артифакты при выводе изображения. На данном этапе, само изображение не готово для вывода на Surface, мы просто передаем известные данные декодеру.

Цикл декодирования


Цикл декодирования включает в себя запрос буфера данных который доступен на выходе, если доступен, можно его необходимо вернуть системе и сообщить если мы желаем рендерить данный кадр на Surface.

@Override
public void run() {
    try {
        BufferInfo info = new BufferInfo(); // переиспользуем BufferInfo
        while (mRunning) {
            if (mConfigured) { // если кодек готов
                int index = mDecoder.dequeueOutputBuffer(info, mTimeoutUs);
                if (index >= 0) { // буфер с индексом index доступен
                    // info.size > 0: если буфер не нулевого размера, то рендерим на Surface
                    mDecoder.releaseOutputBuffer(index, info.size > 0);
                    // заканчиваем работу декодера если достигнут конец потока данных
                    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                        mRunning = false;
                        break;
                    }
                }
            } else {
                // просто спим, т.к. кодек не готов
                try {
                    Thread.sleep(10);
                } catch (InterruptedException ignore) {
                }
            }
        }
    } finally {
       // освобождение ресурсов
        release();
    }
}

Освобождение ресурсов


Довольно таки схожий процесс, но все же

void release() {
    if (mConfigured) {
        mDecoder.stop();
        mDecoder.release();
    }
}

Итог


Надеюсь материал был доступен и не сильно труден для понимания. MediaCodec хороший API, который дает унифицированный доступ к медия системе на различных устройствах.
Tags:
Hubs:
Total votes 18: ↑17 and ↓1+16
Comments5

Articles