MediaMuxer+MediaCodec生成MP4视频黑屏

发表这篇文章目的是为了记录一次解决Android开发中遇到的问题,总结解决思路及心得.这里要特别感谢指导我的刘老师,新项目的领导.

现象:配置(CPU)稍微偏低的手机生成视频播放时为黑屏.
初步分析:为写入视频时出错导致.
分析的思路如下:

下面是音视频混合代码:
EncoderVideoRunnable和MediaMuxerRunnable是两个线程,前者生成编码后的视频数据,后者将视频数据写入文件.

(AiMediaMuxer.java)
private class MediaMuxerRunnable implements Runnable {

        @Override
        public void run() {
            initMuxer();
            baseTimeStamp = System.nanoTime();
            while (!isExit) {
                // 混合器没有启动或数据缓存为空,则阻塞混合线程等待启动(数据输入)
                if (isMuxerStarted) {
                    // 从缓存读取数据写入混合器中
                    if (mMuxerDatas.isEmpty()) {
//                        PaDebugUtil.i(TAG, "run--->混合器没有数据,阻塞线程等待");
                        synchronized (lock) {
                            try {
                                lock.wait();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    } else {
                        MuxerData data = mMuxerDatas.remove(0);
                        if (data != null) {
                            int track = 0;
                            try {
                                if (data.trackIndex == TRACK_VIDEO) {
                                    track = videoTrack;
//                                    PaDebugUtil.d(TAG, "---写入视频数据---");
                                } else if (data.trackIndex == TRACK_AUDIO) {
//                                    PaDebugUtil.d(TAG, "---写入音频数据---");
                                    track = audioTrack;
                                }
//                                PaDebugUtil.d(TAG, "before SampleData presentationTimeUs: "+data.bufferInfo.presentationTimeUs);
                                mMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
                                prevOutputPTSUs = data.bufferInfo.presentationTimeUs;
                            } catch (Exception e) {
                                PaDebugUtil.e(TAG, "写入数据到混合器失败,track=" + track);
                                e.printStackTrace();
                            }
                        }
                    }
                } else {
                    PaDebugUtil.i(TAG, "run--->混合器没有启动,阻塞线程等待");
                    synchronized (lock) {
                        try {
                            lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            stopMuxer();
        }
    }

其中mMuxerDatas为自定义混合器数据集合,便于MediaMuxer.writeSampleData()使用.

private Vector mMuxerDatas;
/**
    * 封装要混合器数据实体
     */
public static class MuxerData {
        int trackIndex;
        ByteBuffer byteBuf;
        MediaCodec.BufferInfo bufferInfo;

        public MuxerData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo) {
            this.trackIndex = trackIndex;
            this.byteBuf = byteBuf;
            this.bufferInfo = bufferInfo;
        }
    }

组装数据的地方:

(EncoderVideoRunnable.java)
@SuppressLint("NewApi")
    private void encoderBytes(byte[] rawFrame) {

        ByteBuffer[] inputBuffers = mVideoEncodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mVideoEncodec.getOutputBuffers();

        //返回编码器的一个输入缓存区句柄,-1表示当前没有可用的输入缓存区
        int inputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
        if (inputBufferIndex >= 0) {
            // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端
            ByteBuffer inputBuffer = null;
            if (!isLollipop()) {
                inputBuffer = inputBuffers[inputBufferIndex];
            } else {
                inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
            }
            // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理
            inputBuffer.clear();
            inputBuffer.put(rawFrame);
            mVideoEncodec.queueInputBuffer(inputBufferIndex, 0, rawFrame.length, getPTSUs(), 0);
        }

        // 返回一个输出缓存区句柄,当为-1时表示当前没有可用的输出缓存区
        // mBufferInfo参数包含被编码好的数据,timesOut参数为超时等待的时间
        int outputBufferIndex = -1;
        MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
        do {
            outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//        PaDebugUtil.i(TAG, "获得编码器输出缓存区超时");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // 如果API小于21,APP需要重新绑定编码器的输入缓存区;
                // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
                if (!isLollipop()) {
                    outputBuffers = mVideoEncodec.getOutputBuffers();
                }
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次
                // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步)
                MediaFormat newFormat = mVideoEncodec.getOutputFormat();
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                if (mMuxerUtils != null) {
                    mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
                    PaDebugUtil.i(TAG, "编码器输出缓存区格式改变,添加视频轨道到混合器");
                }
            } else {
                // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
                ByteBuffer outputBuffer = null;
                if (!isLollipop()) {
                    outputBuffer = outputBuffers[outputBufferIndex];
                } else {
                    outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
                }
                // 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
                // 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
                if (isKITKAT()) {
                    outputBuffer.position(mBufferInfo.offset);
                    outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                }
                // 根据NALU类型判断帧类型
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                int type = outputBuffer.get(4) & 0x1F;
//        PaDebugUtil.d(TAG, "------还有数据---->" + type);
                if (type == 7 || type == 8) {
//          PaDebugUtil.e(TAG, "------PPS、SPS帧(非图像数据),忽略-------");
                    mBufferInfo.size = 0;
                } else if (type == 5) {
                    // 录像时,第1秒画面会静止,这是由于音视轨没有完全被添加
                    // Muxer没有启动
//          PaDebugUtil.e(TAG, "------I帧(关键帧)-------");
                    if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//            mBufferInfo.presentationTimeUs = getPTSUs();
                        mMuxerUtils.addPreviewData(
                                new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                        isAddKeyFrame = true;
//            PaDebugUtil.e(TAG, "----------->添加关键帧到混合器");
                    }
                } else {
                    if (isAddKeyFrame) {
//            PaDebugUtil.d(TAG, "------非I帧(type=1),添加到混合器-------");
                        if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//              mBufferInfo.presentationTimeUs = getPTSUs();
                            mMuxerUtils.addPreviewData(
                                    new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                            prevPresentationTimes = mBufferInfo.presentationTimeUs;
//              PaDebugUtil.d(TAG, "------添加到混合器");
                        }
                    }
                }
                // 处理结束,释放输出缓存区资源
                mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);

                outputBuffer = null;
//        outputBuffers = null;
//        System.gc();
            }
        } while (outputBufferIndex >= 0);
    }

录制过程中,我们发现黑屏的视频在MediaMuxer.writeSampleData()方法中catch到了异常:
MediaAdapter: "pushBuffer called before start"
我们找到MediaAdapter源码(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MediaAdapter.cpp)抛出异常的地方:

status_t MediaAdapter::pushBuffer(MediaBuffer *buffer) {
    if (buffer == NULL) {
        ALOGE("pushBuffer get an NULL buffer");
        return -EINVAL;
    }

    Mutex::Autolock autoLock(mAdapterLock);
    if (!mStarted) {
        ALOGE("pushBuffer called before start");
        return INVALID_OPERATION;
    }
    mCurrentMediaBuffer = buffer;
    mBufferReadCond.signal();

    ALOGV("wait for the buffer returned @ pushBuffer! %p", buffer);
    mBufferReturnedCond.wait(mAdapterLock);

    return OK;
}

这里写明是mStarted = false的时候会抛出异常,往上查找到是调用了stop()方法后才置为false,那这里可以猜想到肯定是其他地方调用了stop()方法才导致的,那什么情况下会调用stop呢?
我们继续看到adb日志里有一条:
MPEG4Writer:"do not support out of order frames (timestamp: 1892312322 < 1892312350"
我们找到MPEG4Writer源码(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MPEG4Writer.cpp)抛出异常的地方:

currDurationTicks =
    ((timestampUs * mTimeScale + 500000LL) / 1000000LL -
        (lastTimestampUs * mTimeScale + 500000LL) / 1000000LL);
if (currDurationTicks < 0ll) {
    ALOGE("do not support out of order frames (timestamp: %lld < last: %lld for %s track",
            (long long)timestampUs, (long long)lastTimestampUs, trackName);
    copy->release();
    mSource->stop();
    return UNKNOWN_ERROR;
}

通过阅读源码,我们发现这个时间戳应该是底层写入视频数据时的时间戳,即我们在writeSampleData()方法中传入的BufferInfo的presentationTimeUs的值做了一些换算.
我们实现视频数据写入的逻辑中看到,EncoderVideoRunnable线程负责将编码好的视频数据交给MediaMuxerRunnable线程写入文件.初步分析应该是BufferInfo的presentationTimeUs在什么地方被修改了,然后我们在writeSampleData和new MediaCodec.BufferInfo()这两个地方都打印了BufferInfo的内存地址和presentationTimeUs,然后发现在写入视频信息的时候BufferInfo的presentationTimeUs并不是上一次写入的时间戳,
这里插入一段逻辑:

// 向MediaMuxer添加录屏数据
    public void addPreviewData(MuxerData data) {
        if (needAddKeyPreviewData && (data.byteBuf.get(4) & 0x1F) != 5) {
            return;
        }
        needAddKeyPreviewData = false;

        if (isStopWriteDate || isReacordingScreen) {
            return;
        }
        if (mMuxerDatas == null) {
            PaDebugUtil.e(TAG, "添加数据失败");
            return;
        }
        data.bufferInfo.presentationTimeUs = getPTSUs();
        mMuxerDatas.add(data);
        // 解锁
        synchronized (lock) {
            lock.notify();
        }
    }

/**
     * 获取下一个编码的 presentationTimeUs
     * @return
     */
    public  long getPTSUs() {
        //long result = System.nanoTime() / 1000L;
        long result = System.nanoTime();
        // presentationTimeUs should be monotonic
        // otherwise muxer fail to write
        long time = (result - pauseDelayTime) / 1000;

        if (time < prevOutputPTSUs){
            return  prevOutputPTSUs;
        }

        return time;
    }

会判断一次当前时间戳与上一次写入视频信息的时间戳做一个比较取最大值,因而prevOutputPTSUs不可能比上一次小,那么问题就出在当前presentationTimeUs在赋值正确的时间戳后去写入视频信息的时候,这个presentationTimeUs被更改了,这里的BufferInfo对象其实是在EncoderVideoRunnable中创建的,当EncoderVideoRunnable中dequeueOutputBuffer的时候会被更改.
常规CPU运行情况下,这种几率几乎可以忽略不计,但是少数性能稍微差的手机就会大概率出现这种情况了.
这个时候只需要在dequeueOutputBuffer的时候,每次都创建一个新的BufferInfo对象,这样就不会影响写入的时候BufferInfo的presentationTimeUs被修改了.
修改后的EncoderVideoRunnable代码:

do {
            MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
            outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//        PaDebugUtil.i(TAG, "获得编码器输出缓存区超时");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // 如果API小于21,APP需要重新绑定编码器的输入缓存区;
                // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
                if (!isLollipop()) {
                    outputBuffers = mVideoEncodec.getOutputBuffers();
                }
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次
                // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步)
                MediaFormat newFormat = mVideoEncodec.getOutputFormat();
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                if (mMuxerUtils != null) {
                    mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
                    PaDebugUtil.i(TAG, "编码器输出缓存区格式改变,添加视频轨道到混合器");
                }
            } else {
                // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
                ByteBuffer outputBuffer = null;
                if (!isLollipop()) {
                    outputBuffer = outputBuffers[outputBufferIndex];
                } else {
                    outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
                }
                // 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
                // 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
                if (isKITKAT()) {
                    outputBuffer.position(mBufferInfo.offset);
                    outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                }
                // 根据NALU类型判断帧类型
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                int type = outputBuffer.get(4) & 0x1F;
//        PaDebugUtil.d(TAG, "------还有数据---->" + type);
                if (type == 7 || type == 8) {
//          PaDebugUtil.e(TAG, "------PPS、SPS帧(非图像数据),忽略-------");
                    mBufferInfo.size = 0;
                } else if (type == 5) {
                    // 录像时,第1秒画面会静止,这是由于音视轨没有完全被添加
                    // Muxer没有启动
//          PaDebugUtil.e(TAG, "------I帧(关键帧)-------");
                    if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//            mBufferInfo.presentationTimeUs = getPTSUs();
                        mMuxerUtils.addPreviewData(
                                new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                        isAddKeyFrame = true;
//            PaDebugUtil.e(TAG, "----------->添加关键帧到混合器");
                    }
                } else {
                    if (isAddKeyFrame) {
//            PaDebugUtil.d(TAG, "------非I帧(type=1),添加到混合器-------");
                        if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//              mBufferInfo.presentationTimeUs = getPTSUs();
                            mMuxerUtils.addPreviewData(
                                    new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                            prevPresentationTimes = mBufferInfo.presentationTimeUs;
//              PaDebugUtil.d(TAG, "------添加到混合器");
                        }
                    }
                }
                // 处理结束,释放输出缓存区资源
                mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);

                outputBuffer = null;
//        outputBuffers = null;
//        System.gc();
            }
        } while (outputBufferIndex >= 0);

其实只是在dequeueOutputBuffer前每次都创建新的BufferInfo.
改完运行,发现问题解决了,呼呼...

最后,总结一下从发现问题到解决问题的全过程:
1,遇到问题不要觉得太难还没开始就放弃思考,如果最后没有解决问题,但是分析思路的养成也是非常重要.
2,尽量多分析源码,对解决问题事半功倍.
3,代码大忌生搬硬套,网上大手也有写bug的情况,代码抄过来要分析每一步的逻辑,养成好的编码习惯.

你可能感兴趣的:(MediaMuxer+MediaCodec生成MP4视频黑屏)