Мультимедийный аудио 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 оставил полезный комментарий, связанный с этим поведением.
Ответы
Ответ 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);