使用Android的MediaCodec和MediaMuxer进行AAC音频复用

18
我正在修改一个Android Framework example,将MediaCodec生成的elementary AAC流打包到独立的.mp4文件中。我使用了单个MediaMuxer实例,其中包含一个由MediaCodec实例生成的AAC轨道。
然而,每当我调用mMediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)时,都会最终收到错误消息: E/MPEG4Writer﹕timestampUs 0 < lastTimestampUs XXXXX for Audio trackmCodec.queueInputBuffer(...)中排队原始输入数据时,我按照Framework Example提供0作为时间戳值(我还尝试使用单调递增的时间戳值,但结果相同)。我已经成功地使用相同的方法将原始相机帧编码为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(...),但在调用MediaCodec.dequeueOutputBuffer(info, timeoutUs)后收到的是info.presentationTimeUs=33219。 fadden留下了一个有关此行为的有用评论。

1
听起来像是MediaMuxer得到了零和非零时间戳。你尝试过在每个writeSampleData调用时记录info的内容以验证它是否具有您期望的值吗? - fadden
我记录了输出,确实,在错误被抛出之前,info包含一个非零的presentationTimeUs。这个值如何与提供给queueInputBuffer(...)的值不同? - dbro
1
我不知道。这个值似乎是从先前的值固定偏移量 - 也就是说,它每次都是相同的值,但如果你为时间戳传递一个恒定的非零值,它会发生变化吗? - fadden
是的,未解释的时间戳总是与我提供的常量时间戳相差固定值:23219。 - dbro
2
最可能的情况是编码器正在处理输出 -- 可能将输入数据包分成两个输出数据包 -- 这需要它合成时间戳。它会获取数据包开始时的时间戳,并根据比特率和字节数添加一个值。如果您正确生成呈现时间的时间戳,则不应在“中间”时间戳生成时看到其向后倒退。 - fadden
3个回答

7
感谢fadden的帮助,我在Github上得到了一个概念验证 音频编码器视频+音频编码器。总之:

AudioRecord 的样本发送到 MediaCodec + MediaMuxer 包装器中。在 audioRecord.read(...) 处使用系统时间作为音频时间戳,只要您经常轮询以避免填满 AudioRecord 的内部缓冲区(以避免在调用 read 时和 AudioRecord 记录样本的时间之间发生漂移)。太糟糕了,AudioRecord 不直接通信时间戳...

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

请注意,AudioRecord 仅保证支持16位PCM样本,但MediaCodec.queueInputBufferbyte[]形式接受输入。将byte[]传递给audioRecord.read(dataBuffer,...)将会截断将16位样本分割成8位。
我发现这种轮询方式仍然偶尔会生成timestampUs XXX < lastTimestampUs XXX for Audio track错误,因此我包含了一些逻辑来跟踪由mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutMs)报告的bufferInfo.presentationTimeUs并在调用mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)之前进行必要的调整。

我已经成功编码了我的相机预览流的previewTextures - 也使用了fadden的示例代码...但是当我使用相当高的比特率(从~5.000.000开始)时,“writeSampleData”偶尔会产生约500毫秒的延迟...你有什么想法出了问题吗? - mAx
我已经在这个问题上创建了一个新的带有更多细节的问题: https://dev59.com/9XfZa4cB1Zd3GeqPX_P9 - mAx
1
样本不会被截断。截断是指每个16位帧被缩短为8位帧,但实际上并非如此,每个16位帧被分成两个字节,但这可能只是语义问题。 - HPP
@dragonfly 我的结果:https://github.com/kickflip/kickflip-android-sdk。请查看 AVRecorder.javaCameraEncoder.javaMicrophoneEncoder.java - dbro
@dbro 感谢您的慷慨,我已经下载了您的代码。我发现您使用了“getJitterFreePTS”来修改系统当前时间,当将pcm数据发送到音频编码器时。为什么要修改时间戳,并且为什么要使用“audioInputLength / 2”作为“getJitterFreePTS”的第二个参数? - dragonfly
显示剩余4条评论

4
上述回答中的代码https://dev59.com/1-o6XIcBkEYKwwoYLhTO#18966374还会出现“音频轨道的时间戳必须满足timestampUs XXX < lastTimestampUs XXX”的错误,因为如果您从AudioRecord的缓冲区读取速度过快,则生成的时间戳之间的持续时间会比实际音频样例之间的持续时间小。

因此,我对解决此问题的方法是生成第一个时间戳,并使每个下一个样本的时间戳增加您的样本持续时间(取决于位率、音频格式、声道配置)。

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);
                }
            }
        };

2
最佳答案...一个评论:如果您使用short[]轮询AudioRecord的缓冲区,则行BUFFER_DURATION_US = 1_000_000 *(ARR_SIZE / AUDIO_CHANNELS)/ SAMPLE_AUDIO_RATE_IN_HZ是正确的。如果您使用byte[],则该行变为:BUFFER_DURATION_US = 1_000_000 *(ARR_SIZE / AUDIO_CHANNELS)/ SAMPLE_AUDIO_RATE_IN_HZ / 2; - Alexandru Circus
哦,是的,我忘了指定数组类型。答案已经更新,谢谢@AlexandruCircus! - Oleg Sokolov
@OlehSokolov 先生有问题 https://dev59.com/2bHma4cB1Zd3GeqPTfW0 - Ashvin solanki
ARR_SIZE是什么? - Matt Wolfe
@MattWolfe 一个数组大小。这个数组将用于从AudioRecord中读取样本。 - Oleg Sokolov

0
问题出现是因为您无序接收缓冲区: 尝试添加以下测试:
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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接