前言
这节学习视频开发的一些基础操作,具体包括使用MediaRecorder来录制视频,采集视频数据并保存为mp4文件。我学习的教程里使用的是Camera,通过回调来获取到NV21数据,这个获取的数据更加原始。
使用SurfaceView来预览,也可以使用TextureView来预览,但是我发现TextureView在手机上使用时存在卡顿的情况,可能是不支持硬件加速。
最开始使用MediaExtractor来解析视频时,我还以为可以直接将mp4文件输出单独的音频文件和视频文件,最后发现需要结合MediaMuxer将取出的数据封装才可以使用。
权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
XML文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="开始录制"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="停止录制"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btStart" />
<Button
android:id="@+id/btParsing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="拆分视频"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btStop" />
<Button
android:id="@+id/btSynthetic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="合成视频"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btParsing" />
<SurfaceView
android:id="@+id/surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MediaRecorder录制流程
1.使用Camera.open()来打开相机。
2.初始化MediaRecorder,设置编码格式、封装格式、码率等。
3.添加surface预览。
4.调用prepare函数后调用start方法开始录制视频。
5.调用stop方法,并释放资源。
binding.btStart.setOnClickListener(view -> {
binding.surface.setVisibility(View.VISIBLE);
camera = Camera.open();
camera.setDisplayOrientation(90);
camera.unlock();
mediaRecorder = new MediaRecorder();
mediaRecorder.setCamera(camera);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setVideoFrameRate(60);
File file = new File(filePath);
mediaRecorder.setOutputFile(file);
mediaRecorder.setVideoSize(1920, 1080);
mediaRecorder.setOrientationHint(90);
mediaRecorder.setPreviewDisplay(binding.surface.getHolder().getSurface());
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mediaRecorder.start();
});
binding.btStop.setOnClickListener(view -> {
binding.surface.setVisibility(View.INVISIBLE);
mediaRecorder.stop();
mediaRecorder.release();
camera.stopPreview();
camera.release();
});
MediaExtractor简介
MediaExtractor的主要作用时将音频和视频数据分离,主要api如下。
-
setDataSource(String path):设置文件地址,支持的类型挺多的。 -
getTrackCount():得到源文件通道数,包括音频和视频。 -
getTrackFormat(int i):获取指定的通道格式,包含其通道的很多配置信息。 -
readSampleData(ByteBuffer byteBuf, int offset):把制定通道的数据按偏移量读取到ByteBuffer中。 -
selectTrack(int i):选定特定的轨道,会影响 readSampleData(ByteBuffer, int), getSampleTrackIndex() and getSampleTime()的输出,这三个函数输出的是选定轨道的信息。(特别注意!!!) -
advance():读取下一帧数据。
MediaMuxer简介
通过MediaExtractor得到数据,然后使用MediaMuxer可以将其单独封装成视频和音频文件,还能将音频和视频混合成一个音视频文件。其主要api如下:
-
MediaMuxer(String path, int format):初始化MediaMuxer,path为输出文件的名称,format为输出文件的格式。 -
addTrack(MediaFormat format):添加通道,通常使用Extractor.getTrackFormat(int index)来获取MediaFormat。 -
writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道。
视频解析与封装流程
解析和封装的关系可以说“焦不离孟,孟不离焦”,我们先将录制的视频解析封装成两个独立的文件,都可以播放的文件,然后在将两者混合成一个音视频文件。(当然这是一种比较学习的做法,通常是解析以后,直接将源数据封装成mp4文件)
1.初始化两个MediaExtractor,一个为videoExtractor,另一个为audioExtractor。
2.初始化MediaMuxer,设置其输出格式为:MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4。
3.使用videoExtractor获取视频文件的视频通道,使用audioExtractor获取音频文件的音频通道。
4.开始合成,设置Buffer。
5.通过mediaExtractor.readSampleData读取数据流,然后把通过mediaExtractor解封装的数据通过writeSampleData写入到对应的轨道。
6.封装合成完成,释放资源。
@SuppressLint("WrongConstant")
private void packageMp4(String videoPath, String audioPath){
String outFile = Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Audio/v/2.mp4";
MediaExtractor videoExtractor = new MediaExtractor();
MediaExtractor audioExtractor = new MediaExtractor();
try {
MediaMuxer mediaMuxer = new MediaMuxer(outFile,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoTrack = -1;
int audioTrack = -1;
videoExtractor.setDataSource(videoPath);
for (int i=0;i<videoExtractor.getTrackCount();i++){
String mineType = videoExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME);
if (mineType.startsWith("video/")){
MediaFormat mediaFormat = videoExtractor.getTrackFormat(i);
videoExtractor.selectTrack(i);
videoTrack = mediaMuxer.addTrack(mediaFormat);
}
}
audioExtractor.setDataSource(audioPath);
for (int i=0;i<videoExtractor.getTrackCount();i++){
String mineType = audioExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME);
if (mineType.startsWith("audio/")){
MediaFormat mediaFormat = audioExtractor.getTrackFormat(i);
audioTrack = mediaMuxer.addTrack(mediaFormat);
audioExtractor.selectTrack(i);
}
}
mediaMuxer.start();
ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);
MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
while (true) {
int readSampleCount = videoExtractor.readSampleData(byteBuffer, 0);
if (readSampleCount < 0) {
break;
}
videoBufferInfo.flags = videoExtractor.getSampleFlags();
videoBufferInfo.offset = 0;
videoBufferInfo.size = readSampleCount;
videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
mediaMuxer.writeSampleData(videoTrack,byteBuffer,videoBufferInfo);
videoExtractor.advance();
}
MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
while (true) {
int readSampleCount = audioExtractor.readSampleData(byteBuffer, 0);
if (readSampleCount < 0) {
break;
}
audioBufferInfo.flags = audioExtractor.getSampleFlags();
audioBufferInfo.offset = 0;
audioBufferInfo.size = readSampleCount;
audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
mediaMuxer.writeSampleData(audioTrack,byteBuffer,audioBufferInfo);
audioExtractor.advance();
}
videoExtractor.release();
audioExtractor.release();
mediaMuxer.stop();
mediaMuxer.release();
} catch (IOException e) {
e.printStackTrace();
}
}
注意
1.mediamuxer只支持aac格式的音频数据,不支持mp3,使用mp3时需要先使用ffmpeg截取和转换,当然这个我也不会。
2.使用这个mediaMuxer.writeSampleData方法时,一定要注意的一个参数trackIndex,这个参数在纯音频或纯视频时只有一个轨道。
结语
这期使用目前最用心的一期,从录制到预览到解析合成我走了很多弯路,还好有其他博客得以借鉴。其实我也偷了个懒,我的学习pdf里录制使用然后处理原始数据,但是它没有具体的代码可以参考,只提出了一个思路。
上班了,更新频率不会那么快。
本期博客参考:
灰色飘零博客园
知乎文章
需要源码的盆友也可以访问我的gitlub,源码
|