Кодирование H.264 с камеры с помощью Android MediaCodec

Я пытаюсь заставить это работать на Android 4.1 (используя обновленный планшет Asus Transformer). Благодаря ответу Алекса на мой предыдущий вопрос я уже смог записать некоторые необработанные данные H.264 в файл, но этот файл можно воспроизвести только с ffplay -f h264, и кажется, что он потерял всю информацию о частоте кадров (чрезвычайно быстрое воспроизведение). Также цветовое пространство выглядит неправильным (атм использует камеру по умолчанию на стороне энкодера).

public class AvcEncoder {

private MediaCodec mediaCodec;
private BufferedOutputStream outputStream;

public AvcEncoder() { 
    File f = new File(Environment.getExternalStorageDirectory(), "Download/video_encoded.264");
    touch (f);
    try {
        outputStream = new BufferedOutputStream(new FileOutputStream(f));
        Log.i("AvcEncoder", "outputStream initialized");
    } catch (Exception e){ 
        e.printStackTrace();
    }

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 320, 240);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
}

public void close() {
    try {
        mediaCodec.stop();
        mediaCodec.release();
        outputStream.flush();
        outputStream.close();
    } catch (Exception e){ 
        e.printStackTrace();
    }
}

// called from Camera.setPreviewCallbackWithBuffer(...) in other class
public void offerEncoder(byte[] input) {
    try {
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input);
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
            byte[] outData = new byte[bufferInfo.size];
            outputBuffer.get(outData);
            outputStream.write(outData, 0, outData.length);
            Log.i("AvcEncoder", outData.length + " bytes written");

            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        }
    } catch (Throwable t) {
        t.printStackTrace();
    }

}

Изменение типа кодировщика на «video/mp4», по-видимому, решает проблему частоты кадров, но, поскольку основная цель — создать потоковую службу, это не очень хорошее решение.

Я знаю, что я удалил часть кода Алекса, учитывая SPS и PPS NALU, но я надеялся, что в этом нет необходимости, поскольку эта информация также поступала от outData, и я предполагал, что кодировщик отформатирует ее правильно. Если это не так, как мне расположить различные типы NALU в моем файле/потоке?

Итак, что мне здесь не хватает, чтобы создать правильный, работающий поток H.264? И какие настройки следует использовать для согласования цветового пространства камеры и цветового пространства кодировщика?

У меня такое ощущение, что это больше вопрос, связанный с H.264, чем тема Android/MediaCodec. Или я все еще неправильно использую MediaCodec API?

Заранее спасибо.


person gleerman    schedule 19.11.2012    source источник
comment
У меня было много проблем с медиаплеером Android, а также с тем, как разные телефоны Android ведут себя по-разному. Возможно ли сделать конверсию на стороне сервера?   -  person David T.    schedule 19.11.2012
comment
Я работаю над аналогичной функциональностью, но в настоящее время получаю исключение java.nio.BufferOverflowException при вызове offerEncoder(...) из Camera.setPreviewCallbackWithBuffer(...), не могли бы вы поделиться подходом, который вы использовали в Camera.setPreviewCallbackWithBuffer( ...). Большое спасибо.   -  person lucasweb    schedule 08.02.2013
comment
Код выглядит нормально, за исключением java.nio.BufferOverflowException, которое происходит на inputBuffer.put(input); утверждение. Попытка разбить байт на куски buffer.capacity() закончилась ошибкой IllegalStateException при вызове MediaCodec.queueInputBuffer. Любая идея, как это можно исправить?   -  person Nar Gar    schedule 15.02.2013
comment
Решили проблему со скоростью? Если да, то как?   -  person Nazar Merza    schedule 22.01.2014


Ответы (5)


Для вашего быстрого воспроизведения - проблема с частотой кадров, здесь вам ничего не нужно делать. Поскольку это потоковое решение, другой стороне необходимо заранее сообщать частоту кадров или метки времени для каждого кадра. Оба они не являются частью элементарного потока. Либо выбирается предопределенная частота кадров, либо вы передаете какой-то sdp или что-то в этом роде, либо используете существующие протоколы, такие как rtsp. Во втором случае временные метки являются частью потока, отправляемого в виде чего-то вроде rtp. Затем клиент должен отдать поток rtp и воспроизвести его обратно. Вот как работает элементарная потоковая передача. [либо зафиксируйте частоту кадров, если у вас кодировщик с фиксированной частотой, либо укажите временные метки]

Воспроизведение на локальном ПК будет быстрым, потому что он не будет знать частоту кадров. Указав параметр fps перед вводом, например

ffplay -fps 30 in.264

Вы можете управлять воспроизведением на ПК.

Что касается файла, который не воспроизводится: есть ли в нем SPS и PPS. Также у вас должны быть включены заголовки NAL - формат приложения b. Я мало что знаю об Android, но это требование для воспроизведения любого элементарного потока h.264, когда они не находятся в каких-либо контейнерах, и их нужно сбросить и воспроизвести позже. Если для Android по умолчанию используется mp4, но заголовки приложенияb по умолчанию будут отключены, поэтому, возможно, есть переключатель для его включения. Или, если вы получаете данные кадр за кадром, просто добавьте их самостоятельно.

Что касается цветового формата: я думаю, по умолчанию должно работать. Так что попробуй не ставить. Если нет, то попробуйте форматы 422 Planar или UVYV/VYUY чередующиеся. обычно камеры являются одним из них. (но не обязательно, это могут быть те, с которыми я сталкивался чаще).

person av501    schedule 24.11.2012

Android 4.3 (API 18) предлагает простое решение. Класс MediaCodec теперь принимает входные данные от Surfaces, что означает, что вы можете подключить предварительный просмотр поверхности камеры к кодировщику и обойти все странные проблемы с форматом YUV.

Существует также новый класс MediaMuxer, который преобразует необработанный поток H.264 в формат .mp4. файл (при желании смешиваясь с аудиопотоком).

См. исходный код CameraToMpegTest для примера выполнения именно этого. (Также демонстрируется использование фрагментного шейдера OpenGL ES для простого редактирования видео во время его записи.)

person fadden    schedule 24.07.2013
comment
Я изменил пример набора тестов совместимости CameraToMpegTest в обычное действие. github.com/OnlyInAmerica/HWEncoderExperiments - person dbro; 12.09.2013
comment
И теперь в Grafika есть пример показа + захват камеры (github.com/google/grafika), который записывает предварительный просмотр камеры в формате .mp4 с одновременным отображением его на экране. - person fadden; 20.01.2014
comment
@fadden этот mp4 можно транслировать одновременно? - person ingsaurabh; 13.09.2016

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

public static byte[] YV12toYUV420PackedSemiPlanar(final byte[] input, final byte[] output, final int width, final int height) {
        /* 
         * COLOR_TI_FormatYUV420PackedSemiPlanar is NV12
         * We convert by putting the corresponding U and V bytes together (interleaved).
         */
        final int frameSize = width * height;
        final int qFrameSize = frameSize/4;

        System.arraycopy(input, 0, output, 0, frameSize); // Y

        for (int i = 0; i < qFrameSize; i++) {
            output[frameSize + i*2] = input[frameSize + i + qFrameSize]; // Cb (U)
            output[frameSize + i*2 + 1] = input[frameSize + i]; // Cr (V)
        }
        return output;
    }

Or

 public static byte[] YV12toYUV420Planar(byte[] input, byte[] output, int width, int height) {
        /* 
         * COLOR_FormatYUV420Planar is I420 which is like YV12, but with U and V reversed.
         * So we just have to reverse U and V.
         */
        final int frameSize = width * height;
        final int qFrameSize = frameSize/4;

        System.arraycopy(input, 0, output, 0, frameSize); // Y
        System.arraycopy(input, frameSize, output, frameSize + qFrameSize, qFrameSize); // Cr (V)
        System.arraycopy(input, frameSize + qFrameSize, output, frameSize, qFrameSize); // Cb (U)

        return output;
    }
person br1    schedule 16.01.2013
comment
Очень полезно. Ваша вторая функция была именно тем, что мне было нужно. Удалось получить данные предварительного просмотра YV12 и преобразовать их в YUV420Planar для ввода в кодировщик MediaCodec. - person Andrew Cottrell; 21.08.2013
comment
Что-то не так звучит в этом ответе. В первом фрагменте кода имя метода — YV12toYUV420PackedSemiPlanar. Однако в строках ниже вы упоминаете, что COLOR_TI_FormatYUV420PackedSemiPlanar — это NV12. - person Utkarsh Sinha; 24.08.2013
comment
@UtkarshSinha fourcc.org/yuv.php Хорошее чтение, NV12 является ЮВ420. Моя проблема заключалась в сохранении слишком малого количества кадров. Теперь мне нужно увидеть солнце 2moro, чтобы проверить цвет. - person John; 14.10.2015

Вы можете запросить у MediaCodec поддерживаемый формат растрового изображения и запросить предварительный просмотр. Проблема в том, что некоторые медиакодеки поддерживают только проприетарные упакованные форматы YUV, которые вы не можете получить в предварительном просмотре. В частности 2130706688 = 0x7F000100 = COLOR_TI_FormatYUV420PackedSemiPlanar . Формат по умолчанию для предварительного просмотра: 17 = NV21 = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar = YCbCr 420 Semi Planar

person Marcus Wolschon    schedule 05.12.2012
comment
Кроме того, некоторые из них утверждают, что поддерживают один формат, но предоставляют другой (например, NV12/NV21 работают неправильно на многих устройствах Samsung, а некоторые устройства Samsung предоставляют один из форматов YV, когда вы запрашиваете формат NV). Это расстраивает. - person Rob Elsner; 25.11.2014

Если вы явно не запрашивали другой формат пикселей, буферы предварительного просмотра камеры будут поступать в формате YUV 420, известном как NV21, для которого COLOR_FormatYCrYCb является эквивалентом MediaCodec.

К сожалению, как упоминается в других ответах на этой странице, нет гарантии, что на вашем устройстве кодировщик AVC поддерживает этот формат. Обратите внимание, что существуют некоторые странные устройства, которые не поддерживают NV21, но я не знаю ни одного, который можно было бы обновить до API 16 (следовательно, иметь MediaCodec).

В документации Google также утверждается, что YV12 плоский YUV должен поддерживаться в качестве камеры. формат предварительного просмотра для всех устройств с API >= 12. Поэтому может быть полезно попробовать его (эквивалент MediaCodec: COLOR_FormatYUV420Planar, который вы используете в своем фрагменте кода).

Обновление: как напомнил мне Эндрю Коттрелл, YV12 все еще нуждается в замене цветности, чтобы стать COLOR_FormatYUV420Planar.

person Alex Cohn    schedule 16.01.2013
comment
По моему опыту, данные предварительного просмотра YV12 и формат YUV420Planar не эквивалентны. Мне пришлось использовать функцию YV12toYUV420Planar() из другого ответа на этой странице, чтобы преобразовать одно в другое. - person Andrew Cottrell; 21.08.2013