Android-->MediaMuxer,MediaCodec,AudioRecord及Camera实现音频视频混合MP4文件

本文相当长,读者请注意…


阅读之前,我喜欢你已经了解了以下内容:
1:https://github.com/saki4510t/AudioVideoRecordingSample
这个开源库介绍了, 音频和视频的录制, 其实已经够了~~~,不过视频的录制采用的是GLSurfaceView中的Surface方法, 并没有直接采用TextureView和Camera的PreviewCallback方法.

2:https://github.com/google/grafika
这个是谷歌的开源项目,里面介绍了很多关于GLSurfaceView和TextureView的操作,当然也有MediaCodec的使用.

3:https://developer.android.com/reference/android/media/MediaMuxer.html
这个是API文档介绍MediaMuxer混合器的文档,当然~~这个文档真的是”很详细”;

4:https://github.com/icylord/CameraPreview
这个开源库介绍了Camera的使用,还有TextureView,MediaCodec…and so on


能量补充完了,就该到我登场了…

本文的目的是通过Camera的PreviewCallback拿到帧数据,用MediaCodec编码成H264,添加到MediaMuxer混合器打包成MP4文件,并且使用TextureView预览摄像头. 当然使用AudioRecord录制音频,也是通过MediaCodec编码,一样是添加到MediaMuxer混合器和视频一起打包, 这个难度系数很低.

在使用MediaMuxer混合的时候,主要的难点就是控制视频数据和音频数据的同步添加,和状态的判断;

本文所有代码,采用片段式讲解,文章结尾会有源码下载:

1:视频录制和H264的数据获取

Camera mCamera = Camera.open();
mCamera.addCallbackBuffer(mImageCallbackBuffer);//必须的调用1
mCamera.setPreviewCallbackWithBuffer(mCameraPreviewCallback);
...
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    //通过回调,拿到的data数据是原始数据
    videoRunnable.add(data);//丢给videoRunnable线程,使用MediaCodec进行h264编码操作
    camera.addCallbackBuffer(data);//必须的调用2
}

1.1:H264的编码操作

编码器的配置:

private static final String MIME_TYPE = "video/avc"; // H.264的mime类型
MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);//选择系统用于编码H264的编码器信息,固定的调用
mColorFormat = selectColorFormat(codecInfo, MIME_TYPE);//根据MIME格式,选择颜色格式,固定的调用
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE,
        this.mWidth, this.mHeight);//根据MIME创建MediaFormat,固定
//以下参数的设置,尽量固定.当然,如果你非常了解,也可以自行修改
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);//设置比特率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);//设置帧率
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);//设置颜色格式
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);//设置关键帧的时间
try {
    mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());//这里就是根据上面拿到的编码器创建一个MediaCodec了;//MediaCodec还有一个方法可以直接用MIME类型,创建
} catch (IOException e) {
    e.printStackTrace();
}

//第二个参数用于播放MP4文件,显示图像的Surface;
//第四个参数,编码H264的时候,固定CONFIGURE_FLAG_ENCODE, 播放的时候传入0即可;API文档有解释
mMediaCodec.configure(mediaFormat, null, null,
        MediaCodec.CONFIGURE_FLAG_ENCODE);//关键方法
mMediaCodec.start();//必须

开始H264的编码:

private void encodeFrame(byte[] input) {//这个参数就是上面回调拿到的原始数据
    NV21toI420SemiPlanar(input, mFrameData, this.mWidth, this.mHeight);//固定的方法,用于颜色转换

    ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();//拿到输入缓冲区,用于传送数据进行编码
    ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();//拿到输出缓冲区,用于取到编码后的数据
    int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);//得到当前有效的输入缓冲区的索引
    if (inputBufferIndex >= 0) {//当输入缓冲区有效时,就是>=0
        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
        inputBuffer.clear();
        inputBuffer.put(mFrameData);//往输入缓冲区写入数据,关键点
        mMediaCodec.queueInputBuffer(inputBufferIndex, 0,
                mFrameData.length, System.nanoTime() / 1000, 0);//将缓冲区入队
    } else {
    }

    int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);//拿到输出缓冲区的索引
    do {
        if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
        } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            outputBuffers = mMediaCodec.getOutputBuffers();
        } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            //特别注意此处的调用
            MediaFormat newFormat = mMediaCodec.getOutputFormat();
            MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
            if (mediaMuxerRunnable != null) {
            //如果要合成视频和音频,需要处理混合器的音轨和视轨的添加.因为只有添加音轨和视轨之后,写入数据才有效
                mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
            }
        } else if (outputBufferIndex < 0) {
        } else {
            //走到这里的时候,说明数据已经编码成H264格式了
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];//outputBuffer保存的就是H264数据了
            if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                mBufferInfo.size = 0;
            }
            if (mBufferInfo.size != 0) {
                MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();

                //因为上面的addTrackIndex方法不一定会被调用,所以要在此处再判断并添加一次,这也是混合的难点之一
                if (mediaMuxerRunnable.isAudioAdd()) {
                    MediaFormat newFormat = mMediaCodec.getOutputFormat();
                    mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_VIDEO, newFormat);
                }

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

                if (mediaMuxerRunnable != null) {
                //这一步就是添加视频数据到混合器了,在调用添加数据之前,一定要确保视轨和音轨都添加到了混合器
                    mediaMuxerRunnable.addMuxerData(new MediaMuxerRunnable.MuxerData(
                            MediaMuxerRunnable.TRACK_VIDEO, outputBuffer, mBufferInfo
                    ));
                }
            }
            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);//释放资源
        }
        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
    } while (outputBufferIndex >= 0);
}

群友补充1:
上段代码中的NV21toI420SemiPlanar实现方法, 这个编码的视频是黑白的,把这个方法的实现,改为:

private void NV21toI420SemiPlanar(byte[] nv21bytes, byte[] i420bytes, int width, int height) {
    final int iSize = width * height;
    System.arraycopy(nv21bytes, 0, i420bytes, 0, iSize);

    for (int iIndex = 0; iIndex < iSize / 2; iIndex += 2) {
        i420bytes[iSize + iIndex / 2 + iSize / 4] = nv21bytes[iSize + iIndex]; // U
        i420bytes[iSize + iIndex / 2] = nv21bytes[iSize + iIndex + 1]; // V
    }
}

就会是彩色;

群友补充2:
上段代码中的

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

改为

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, System.nanoTime() / 1000);

即可提高成功率.

如果项目中遇到这两个问题,大可以拿去。感谢群友 明天的现在.

2:音频的录制和编码

和视频一样,需要配置编码器:

private static final String MIME_TYPE = "audio/mp4a-latm";
audioCodecInfo = selectAudioCodec(MIME_TYPE);//是不是似曾相识?没错,一样是通过MIME拿到系统对应的编码器信息
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);//CHANNEL_IN_STEREO 立体声
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
//      audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length());
//      audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);

mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
//过程都差不多~不解释了;

获取音频设备,用于获取音频数据:

android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
try {
 final int min_buffer_size = AudioRecord.getMinBufferSize(
         SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
         AudioFormat.ENCODING_PCM_16BIT);
 int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
 if (buffer_size < min_buffer_size)
     buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;

 audioRecord = null;
 for (final int source : AUDIO_SOURCES) {
     try {
         audioRecord = new AudioRecord(
                 source, SAMPLE_RATE,
                 AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size);
         if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
             audioRecord = null;
     } catch (final Exception e) {
         audioRecord = null;
     }
     if (audioRecord != null) break;
 }
} catch (final Exception e) {
 Log.e(TAG, "AudioThread#run", e);
}

开始音频数据的采集:

audioRecord.startRecording();//固定写法
while (!isExit) {
    buf.clear();
    readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME);//读取音频数据到buf
    if (readBytes > 0) {
        buf.position(readBytes);
        buf.flip();
        encode(buf, readBytes, getPTSUs());//开始编码
    }
}

开始音频编码:

private void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) {
 if (isExit) return;
 final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
 final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
     /*向编码器输入数据*/
 if (inputBufferIndex >= 0) {
     final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
     inputBuffer.clear();
     if (buffer != null) {
         inputBuffer.put(buffer);
     }
         mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0,
                 presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     } else {
         mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length,
                 presentationTimeUs, 0);
     }
 } else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
 }
 //上面的过程和视频是一样的,都是向输入缓冲区输入原始数据

 /*获取解码后的数据*/
 ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
 int encoderStatus;
 do {
     encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
     if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
     } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
         encoderOutputBuffers = mMediaCodec.getOutputBuffers();
     } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
      //特别注意此处, 此处和视频编码是一样的
         final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
         MediaMuxerRunnable mediaMuxerRunnable = this.mediaMuxerRunnable.get();
         if (mediaMuxerRunnable != null) {
             //添加音轨,和添加视轨都是一样的调用
             mediaMuxerRunnable.addTrackIndex(MediaMuxerRunnable.TRACK_AUDIO, format);
         }

     } else if (encoderStatus < 0) {
     } else {
         final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
         if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
             mBufferInfo.size = 0;
         }

         if (mBufferInfo.size != 0) {
             mBufferInfo.presentationTimeUs = getPTSUs();
            //当保证视轨和音轨都添加完成之后,才可以添加数据到混合器
             muxer.addMuxerData(new MediaMuxerRunnable.MuxerData(
                     MediaMuxerRunnable.TRACK_AUDIO, encodedData, mBufferInfo));
             prevOutputPTSUs = mBufferInfo.presentationTimeUs;
         }
         mMediaCodec.releaseOutputBuffer(encoderStatus, false);
     }
 } while (encoderStatus >= 0);
}

3:混合器的操作

private Vector<MuxerData> muxerDatas;//缓冲传输过来的数据
public void start(String filePath) throws IOException {
    isExit = false;
    isVideoAdd = false;
//创建混合器
    mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    if (audioRunnable != null) {
        //音频准备工作
        audioRunnable.prepare();
        audioRunnable.prepareAudioRecord();
    }
    if (videoRunnable != null) {
         //视频准备工作
        videoRunnable.prepare();
    }
    new Thread(this).start();
    if (audioRunnable != null) {
        new Thread(audioRunnable).start();//开始音频解码线程
    }
    if (videoRunnable != null) {
        new Thread(videoRunnable).start();//开始视频解码线程
    }

}

//混合器,最重要的就是保证再添加数据之前,要先添加视轨和音轨,并且保存响应轨迹的索引,用于添加数据的时候使用
public void addTrackIndex(@TrackIndex int index, MediaFormat mediaFormat) {
  if (isMuxerStart()) {
      return;
  }
  int track = mediaMuxer.addTrack(mediaFormat);
  if (index == TRACK_VIDEO) {
      videoTrackIndex = track;
      isVideoAdd = true;
      Log.e("angcyo-->", "添加视轨");
  } else {
      audioTrackIndex = track;
      isAudioAdd = true;
      Log.e("angcyo-->", "添加音轨");
  }
  requestStart();
}

private void requestStart() {
   synchronized (lock) {
       if (isMuxerStart()) {
           mediaMuxer.start();//在start之前,确保视轨和音轨已经添加了
           lock.notify();
       }
   }
}

while (!isExit) {
 if (muxerDatas.isEmpty()) {
     synchronized (lock) {
         try {
             lock.wait();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 } else {
     if (isMuxerStart()) {
         MuxerData data = muxerDatas.remove(0);
         int track;
         if (data.trackIndex == TRACK_VIDEO) {
             track = videoTrackIndex;
         } else {
             track = audioTrackIndex;
         }
         //添加数据...
         mediaMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
     }
 }
}

项目源代码: https://github.com/angcyo/PLDroidDemo/tree/master/audiovideorecordingdemo


如果您喜欢这篇文章,您也可以进行打赏, 金额不限.

联系作者

请使用QQ扫码加群, 小伙伴们在等着你哦!

关注我的公众号, 每天都能一起玩耍哦!

©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值