Android декодирует сырой поток h264 с помощью MediaCodec

У меня проблемы с декодированием и отрисовкой необработанных данных h264 с помощью MediaCodec в TextureView. Я получаю необработанные данные в байтовых массивах, каждый массив массива NAL (начинается с 0x00 0x00 0x00 0x01), также есть SPS и PPS NAL в постоянных интервалах. Когда поступают новые данные, я помещаю их в LinkedBlockingQueue:

public void pushData(byte[] videoBuffer) {
    dataQueue.add(videoBuffer);

    if (!decoderConfigured) {
        // we did not receive first SPS NAL unit, we want to throw away all data until we do
        if (dataQueue.peek() != null && checkIfParameterSet(dataQueue.peek(), SPSID)) {

            // SPS NAL unit is followed by PPS NAL unit, we wait until both are present at the
            // start of the queue
            if (dataQueue.size() == 2) {

                // iterator will point head of the queue (SPS NALU),
                // iterator.next() will point PPS NALU
                Iterator<byte[]> iterator = dataQueue.iterator();

                String videoFormat = "video/avc";
                MediaFormat format = MediaFormat.createVideoFormat(videoFormat, width, height);
                format.setString("KEY_MIME", videoFormat);
                format.setByteBuffer("csd-0", ByteBuffer.wrap(concat(dataQueue.peek(), iterator.next())));

                try {
                    decoder = MediaCodec.createDecoderByType(videoFormat);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                decoder.configure(format, mOutputSurface, null, 0);
                decoder.start();

                inputBuffer = decoder.getInputBuffers();

                decoderConfigured = true;
            }
        } else {
            // throw away the data which appear before first SPS NALU
            dataQueue.clear();
        }
    }
}

Как видите, здесь также есть конфигурация декодера. Это делается, когда первый SPS+PPS появляется в очереди. Основная часть работает в while цикл:

private void work() {
    while(true) {
         if (decoderConfigured) {
            byte[] chunk = dataQueue.poll();
            if (chunk != null) {
                // we need to queue the input buffer with SPS and PPS only once
                if (checkIfParameterSet(chunk, SPSID)) {
                    if (!SPSPushed) {
                        SPSPushed = true;
                        queueInputBuffer(chunk);
                    }
                } else if (checkIfParameterSet(chunk, PPSID)) {
                    if (!PPSPushed) {
                        PPSPushed = true;
                        queueInputBuffer(chunk);
                    }
                } else {
                    queueInputBuffer(chunk);
                }
            }

            int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (VERBOSE) Log.d(TAG, "no output from decoder available");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not important for us, since we're using Surface
                if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat newFormat = decoder.getOutputFormat();
                if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
            } else if (decoderStatus < 0) {
                throw new RuntimeException(
                        "unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
            } else { // decoderStatus >= 0
                if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
                        " (size=" + mBufferInfo.size + ")");
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (VERBOSE) Log.d(TAG, "output EOS");
                }

                boolean doRender = (mBufferInfo.size != 0);

                try {
                    if (doRender && frameCallback != null) {
                        Log.d(TAG, "Presentation time passed to frameCallback: " + mBufferInfo.presentationTimeUs);
                        frameCallback.preRender(mBufferInfo.presentationTimeUs);
                    }
                    decoder.releaseOutputBuffer(decoderStatus, doRender);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

И queueInputBuffer выглядит так:

private void queueInputBuffer(byte[] data) {
    int inIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
    if (inIndex >= 0) {
        inputBuffer[inIndex].clear();
        inputBuffer[inIndex].put(data, 0, data.length);
        decoder.queueInputBuffer(inIndex, 0, data.length, System.currentTimeMillis() * 1000, 0);
    }
}

Класс, который оборачивает эту механику, работает в отдельном потоке, аналогично MoviePlayer из графика. Так же FrameCallback является SpeedControlCallback из графика.

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

Когда я искал решение, я обнаружил, что проблема может быть вызвана неправильным временем представления. Я пытался исправить это (вы можете увидеть в коде, я предоставлял системное время вместе с использованием preRender()) без удачи. Но я не совсем уверен, вызвано ли сбой этими временными метками.

Может ли кто-нибудь помочь мне решить эту проблему?

ОБНОВЛЕНИЕ 1

Как предположил Фадден, я проверил свой плеер на данные, созданные самим MediaCodec. Мой код захватил предварительный просмотр камеры, закодировал его и сохранил в файл. Я делал это раньше с помощью канала камеры моего целевого устройства, чтобы я мог просто переключать источник данных. Файл, основанный на предварительном просмотре камеры телефона, не показывает никаких артефактов при воспроизведении. Таким образом, можно сделать вывод, что необработанные данные, поступающие с камеры целевого устройства, обрабатываются (или передаются в декодер) некорректно или несовместимы с MediaCodec (как, возможно, и было предложено в fadden).

Следующим, что я сделал, было сравнение блоков NAL обоих видеопотоков. Видео, закодированное MediaCodec, выглядит так:

0x00, 0x00, 0x00, 0x01, 0x67, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x65, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x21, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x21, 0xNN, 0xNN ...
.
. 
.    
0x00, 0x00, 0x00, 0x01, 0x21, 0xNN, 0xNN ...

Первый NALU происходит только один раз, в начале потока, затем идет второй (с 0x65) и затем кратен с 0x21. Затем снова 0x65, кратно 0x21 и так далее.

Однако камера целевого устройства дает мне это:

0x00, 0x00, 0x00, 0x01, 0x67, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x68, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x61, 0xNN, 0xNN ...
0x00, 0x00, 0x00, 0x01, 0x61, 0xNN, 0xNN ...
.
. 
.    
0x00, 0x00, 0x00, 0x01, 0x61, 0xNN, 0xNN ...

И вся эта последовательность повторяется непрерывно в течение потока.

0 ответов

Другие вопросы по тегам