【问题标题】:Muxing AAC audio with Android's MediaCodec and MediaMuxer使用 Android 的 MediaCodec 和 MediaMuxer 混合 AAC 音频
【发布时间】:2013-09-22 08:30:37
【问题描述】:

我正在修改 Android Framework example 以将 MediaCodec 生成的基本 AAC 流打包成独立的 .mp4 文件。我正在使用一个 MediaMuxer 实例,其中包含一个由 MediaCodec 实例生成的 AAC 轨道。

但是,我在调用mMediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo) 时总是会收到一条错误消息:

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

当我在mCodec.queueInputBuffer(...) 中对原始输入数据进行排队时,我根据框架示例提供 0 作为时间戳值(我还尝试使用具有相同结果的单调递增时间戳值。我已成功将原始相机帧编码为h264/mp​​4 文件,方法相同)。

Check out the full source

最相关的sn-p:

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 留下了与此行为相关的有用评论。

【问题讨论】:

  • 听起来 MediaMuxer 正在获取零和非零时间戳。您是否尝试在每次 writeSampleData 调用时记录 info 的内容以验证它是否具有您期望的值?
  • 我记录了输出,确实,在抛出错误之前,信息包含非零presentationTimeUs。这个值与提供给queueInputBuffer(...) 的值有何不同?
  • 我不知道。该值是否看起来是与先前值的固定偏移量 - 即每次都是相同的值,但如果您为时间戳传递一个恒定的非零值,它会改变?
  • 是的,无法解释的时间戳总是与我提供的固定时间戳不同:23219。
  • 最好的猜测:编码器正在对输出做一些事情——可能将一个输入数据包分成两个输出数据包——这需要它合成一个时间戳。它获取数据包开始的时间戳,并根据比特率和字节数添加一个值。如果您生成的时间戳具有相当正确的呈现时间,则在生成“中间”时间戳时,您不应该看到它倒退。

标签: android android-mediacodec


【解决方案1】:

感谢 fadden 的帮助,我在 Github 上获得了概念验证 audio encodervideo+audio encoder。总结:

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 only guarantees support for 16 bit PCM samples,尽管MediaCodec.queueInputBuffer 将输入作为byte[]。将 byte[] 传递给 audioRecord.read(dataBuffer,...)truncate 将 16 位样本拆分为 8 位。

我发现以这种方式轮询仍然偶尔会产生timestampUs XXX &lt; lastTimestampUs XXX for Audio track 错误,因此我添加了一些逻辑来跟踪mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutMs) 报告的bufferInfo.presentationTimeUs,并在调用mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo) 之前根据需要进行调整。

【讨论】:

  • 我已经设法对我的相机预览流的 previewTextures 进行编码 - 也使用 faddens 示例代码......但是当我使用相当好的比特率(从 ~5.000.000 开始)时,偶尔会出现“writeSampleData”产生大约 500 毫秒的延迟......你知道那里出了什么问题吗?
  • 我已针对此问题创建了一个新问题,并在此处提供了更多详细信息:stackoverflow.com/questions/19361770/…
  • 样本不会被截断。截断将每个 16 位帧缩短为一个 8 位帧,这不是发生的情况,每个 16 位帧被分成两个字节,但这可能只是语义。
  • 我用MediaMuxer和MediaCodec混合音视频成功,可以播放mp4视频文件,但是有问题。例如,我录制了一个12秒的视频,当我用系统播放器播放视频时,播放器显示视频时长为12秒,这是正确的,但播放器需要10秒才能播放到最后。您是否成功地实施了混合,没有任何错误?你是怎么做到的?@dbro
【解决方案2】:

出现问题是因为您收到的缓冲区无序: 尝试添加以下测试:

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

【讨论】:

    【解决方案3】:

    上面答案https://stackoverflow.com/a/18966374/6463821 的代码也提供了timestampUs XXX &lt; lastTimestampUs XXX for Audio track 错误,因为如果你从AudioRecord 的buffer 读取速度比需要的快,生成的时间戳之间的持续时间将小于音频样本之间的实际持续时间。

    所以我对这个问题的解决方案是生成第一个 timstamp 并且每个下一个样本都会根据样本的持续时间增加时间戳 (depends on bit-rate, audio format, channel config).

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

    【讨论】:

    • 最佳答案...一条评论:BUFFER_DURATION_US = 1_000_000 * (ARR_SIZE / AUDIO_CHANNELS) / SAMPLE_AUDIO_RATE_IN_HZ;仅当您使用 short[] 轮询 AudioRecord 的缓冲区时才为真。如果使用 byte[],则行变成这样:BUFFER_DURATION_US = 1_000_000 * (ARR_SIZE / AUDIO_CHANNELS) / SAMPLE_AUDIO_RATE_IN_HZ / 2;
    • 哦,是的,我忘了指定数组类型。答案已经更新,谢谢@AlexandruCircus!
    • @OlehSokolov 先生有问题stackoverflow.com/questions/54765921/…
    • 什么是 ARR_SIZE?
    • @MattWolfe 一个数组大小。用于从 AudioRecord 读取样本的数组
    猜你喜欢
    • 2014-07-21
    • 2021-08-02
    • 2016-04-25
    • 2017-04-22
    • 1970-01-01
    • 2013-11-18
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    相关资源
    最近更新 更多