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 ...
И вся эта последовательность повторяется непрерывно в течение потока.