Подтвердить что ты не робот

Мультимедийный аудио AAC с Android MediaCodec и MediaMuxer

Я изменяю пример Android Framework, чтобы упаковать элементарные потоки AAC, созданные MediaCodec, в автономный файл .mp4. Я использую единственный экземпляр MediaMuxer, содержащий один трек AAC, сгенерированный экземпляром MediaCodec.

Однако я всегда получаю сообщение об ошибке при вызове mMediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo):

E/MPEG4Writer﹕timestampUs 0 < lastTimestampUs XXXXX for Audio track

Когда я помещаю необработанные входные данные в mCodec.queueInputBuffer(...), я предоставляю 0 в качестве значения временной метки для примера Framework (я также пытался использовать монотонно увеличивающиеся значения метки времени с тем же результатом. Я успешно закодировал необработанные рамки камеры для h264/mp4 с этим же методом).

Посмотрите полный источник

Наиболее релевантный фрагмент:

private static void testEncoder(String componentName, MediaFormat format, Context c) {
    int trackIndex = 0;
    boolean mMuxerStarted = false;
    File f = FileUtils.createTempFileInRootAppStorage(c, "aac_test_" + new Date().getTime() + ".mp4");
    MediaCodec codec = MediaCodec.createByCodecName(componentName);

    try {
        codec.configure(
                format,
                null /* surface */,
                null /* crypto */,
                MediaCodec.CONFIGURE_FLAG_ENCODE);
    } catch (IllegalStateException e) {
        Log.e(TAG, "codec '" + componentName + "' failed configuration.");

    }

    codec.start();

    try {
        mMediaMuxer = new MediaMuxer(f.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    } catch (IOException ioe) {
        throw new RuntimeException("MediaMuxer creation failed", ioe);
    }

    ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
    ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();

    int numBytesSubmitted = 0;
    boolean doneSubmittingInput = false;
    int numBytesDequeued = 0;

    while (true) {
        int index;

        if (!doneSubmittingInput) {
            index = codec.dequeueInputBuffer(kTimeoutUs /* timeoutUs */);

            if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
                if (numBytesSubmitted >= kNumInputBytes) {
                    Log.i(TAG, "queueing EOS to inputBuffer");
                    codec.queueInputBuffer(
                            index,
                            0 /* offset */,
                            0 /* size */,
                            0 /* timeUs */,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);

                    if (VERBOSE) {
                        Log.d(TAG, "queued input EOS.");
                    }

                    doneSubmittingInput = true;
                } else {
                    int size = queueInputBuffer(
                            codec, codecInputBuffers, index);

                    numBytesSubmitted += size;

                    if (VERBOSE) {
                        Log.d(TAG, "queued " + size + " bytes of input data.");
                    }
                }
            }
        }

        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        index = codec.dequeueOutputBuffer(info, kTimeoutUs /* timeoutUs */);

        if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
        } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            MediaFormat newFormat = codec.getOutputFormat();
            trackIndex = mMediaMuxer.addTrack(newFormat);
            mMediaMuxer.start();
            mMuxerStarted = true;
        } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            codecOutputBuffers = codec.getOutputBuffers();
        } else {
            // Write to muxer
            ByteBuffer encodedData = codecOutputBuffers[index];
            if (encodedData == null) {
                throw new RuntimeException("encoderOutputBuffer " + index +
                        " was null");
            }

            if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                // The codec config data was pulled out and fed to the muxer when we got
                // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                info.size = 0;
            }

            if (info.size != 0) {
                if (!mMuxerStarted) {
                    throw new RuntimeException("muxer hasn't started");
                }

                // adjust the ByteBuffer values to match BufferInfo (not needed?)
                encodedData.position(info.offset);
                encodedData.limit(info.offset + info.size);

                mMediaMuxer.writeSampleData(trackIndex, encodedData, info);
                if (VERBOSE) Log.d(TAG, "sent " + info.size + " audio bytes to muxer with pts " + info.presentationTimeUs);
            }

            codec.releaseOutputBuffer(index, false);

            // End write to muxer
            numBytesDequeued += info.size;

            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                if (VERBOSE) {
                    Log.d(TAG, "dequeued output EOS.");
                }
                break;
            }

            if (VERBOSE) {
                Log.d(TAG, "dequeued " + info.size + " bytes of output data.");
            }
        }
    }

    if (VERBOSE) {
        Log.d(TAG, "queued a total of " + numBytesSubmitted + "bytes, "
                + "dequeued " + numBytesDequeued + " bytes.");
    }

    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    int inBitrate = sampleRate * channelCount * 16;  // bit/sec
    int outBitrate = format.getInteger(MediaFormat.KEY_BIT_RATE);

    float desiredRatio = (float)outBitrate / (float)inBitrate;
    float actualRatio = (float)numBytesDequeued / (float)numBytesSubmitted;

    if (actualRatio < 0.9 * desiredRatio || actualRatio > 1.1 * desiredRatio) {
        Log.w(TAG, "desiredRatio = " + desiredRatio
                + ", actualRatio = " + actualRatio);
    }


    codec.release();
    mMediaMuxer.stop();
    mMediaMuxer.release();
    codec = null;
}

Обновление: Я нашел корневой симптом, я думаю, находится внутри MediaCodec.:

Я отправляю presentationTimeUs=1000 в queueInputBuffer(...), но получаю info.presentationTimeUs= 33219 после вызова MediaCodec.dequeueOutputBuffer(info, timeoutUs). fadden оставил полезный комментарий, связанный с этим поведением.

4b9b3361

Ответ 1

Благодаря fadden help у меня есть доказательство концепции аудиокодер и видео + аудиокодер на Github. Вкратце:

Отправьте образцы AudioRecord в обертку MediaCodec + MediaMuxer. Использование системного времени в audioRecord.read(...) работает достаточно хорошо, как временная метка аудио, при условии, что вы достаточно часто проводите опрос, чтобы не заполнять внутренний буфер AudioRecord (чтобы избежать дрейфа между временем, которое вы вызываете, и временем, записанным в AudioRecord). Слишком плохо AudioRecord напрямую не связывает временные метки...

// Setup AudioRecord
while (isRecording) {
    audioPresentationTimeNs = System.nanoTime();
    audioRecord.read(dataBuffer, 0, samplesPerFrame);
    hwEncoder.offerAudioEncoder(dataBuffer.clone(), audioPresentationTimeNs);
}

Обратите внимание, что AudioRecord гарантирует только поддержку 16-битных образцов PCM, хотя MediaCodec.queueInputBuffer принимает ввод как byte[]. Передача byte[] в audioRecord.read(dataBuffer,...) будет truncate разбивать 16-разрядные образцы на 8 бит для вас.

Я обнаружил, что опрос таким образом все же иногда генерировал ошибку timestampUs XXX < lastTimestampUs XXX for Audio track, поэтому я включил некоторую логику для отслеживания bufferInfo.presentationTimeUs, сообщенного mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutMs), и при необходимости отрегулировать перед вызовом mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo).

Ответ 2

Код из вышеприведенного ответа fooobar.com/questions/410153/... также предоставляет ошибку timestampUs XXX < lastTimestampUs XXX for Audio track, потому что если вы читаете из AudioRecord` buffer быстрее, чем нужно, продолжительность между генерируемыми timstamp будет меньше реальной продолжительности между образцами аудио.

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

BUFFER_DURATION_US = 1_000_000 * (ARR_SIZE / AUDIO_CHANNELS) / SAMPLE_AUDIO_RATE_IN_HZ;

...

long firstPresentationTimeUs = System.nanoTime() / 1000;

...

audioRecord.read(shortBuffer, OFFSET, ARR_SIZE);
long presentationTimeUs = count++ * BUFFER_DURATION + firstPresentationTimeUs;

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

worker =
        new Thread() {

            @Override
            public void run() {
                try {

                    AudioFrameReader reader =
                            new AudioFrameReader(audioRecord);

                    while (!isInterrupted()) {
                        Thread.sleep(10);

                        addToQueue(
                                reader
                                        .read());
                    }

                } catch (InterruptedException e) {
                    Log.w(TAG, "run: ", e);
                }
            }
        };

Ответ 3

Проблема возникла из-за того, что вы получаете беспорядочные буферы: Попробуйте добавить следующий тест:

if(lastAudioPresentationTime == -1) {
    lastAudioPresentationTime = bufferInfo.presentationTimeUs;
}
else if (lastAudioPresentationTime < bufferInfo.presentationTimeUs) {
    lastAudioPresentationTime = bufferInfo.presentationTimeUs;
}
if ((bufferInfo.size != 0) && (lastAudioPresentationTime <= bufferInfo.presentationTimeUs)) {
    if (!mMuxerStarted) {
        throw new RuntimeException("muxer hasn't started");
    }
    // adjust the ByteBuffer values to match BufferInfo (not needed?)
    encodedData.position(bufferInfo.offset);
    encodedData.limit(bufferInfo.offset + bufferInfo.size);
    mMuxer.writeSampleData(trackIndex.index, encodedData, bufferInfo);
}

encoder.releaseOutputBuffer(encoderStatus, false);