前言
本来计划介绍一下MediaCodec,写Demo的时候发现它要结合其他的API一起使用,所以先延后。这一篇我们先了解一下MediaExtractor和MediaMuxer。
最开始的概念篇已经介绍过了,我们先简单的复习一下:
MediaExtractor
多媒体的提取器,通过它,可以单独操作音视频文件的音频或视频,例如音视频提取,合成之类的操作。
MediaMuxer
多媒体合成器,在功能上与MediaExtractor有相似之处。
正文
今天的案例是断点录制,录制结束后,合成文件并播放。首先我们需要自己打开相机,能够预览摄像头的画面,这种常规操作这里就不做介绍了。这里我们要使用最简单的MediaRecorder实现视频的合成,先对MediaRecorder初始化:
mediaRecorder = MediaRecorder()
mediaRecorder!!.setCamera(camera)
mediaRecorder!!.setOrientationHint(90)
mediaRecorder!!.setAudioSource(MediaRecorder.AudioSource.MIC)
mediaRecorder!!.setVideoSource(MediaRecorder.VideoSource.CAMERA)
// 如果需要设置指定的格式,一定要注意以下API的调用顺序
mediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mediaRecorder!!.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
mediaRecorder!!.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
// 此配置不能和setOutputFormat一起使用
// mediaRecorder!!.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_720P))
mediaRecorder!!.setOutputFile("${saveDir.absoluteFile}/record_${System.currentTimeMillis()}.mp4")
mediaRecorder!!.setPreviewDisplay(surface_view.holder.surface)
// 开始录制
mediaRecorder!!.prepare()
mediaRecorder!!.start()
这里踩了一个坑:
mediaRecorder!!.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_720P))和setOutputFormat不能一起使用。
可能是因为mediaRecorder!!.setProfile()调用了setOutputFormat,如果一起使用setOutputFormat会被调用两次,然后抛出异常,需要注意一下。
另外一定要注意setXXXSource,setOutputFormat,setXXXEncoder的调用顺序,顺序不对直接抛出异常,如果你遇到了IllegalStateException异常,一定要立刻查看一下源码注释,八成是调用顺序出了问题。
image非常简单的页面,我这里做了录制时间的检测,录到足够的时长,开始视频合成。我们先用MediaRecorder录制几个视频,如下图:
image我已经录制好了3个视频,接下来就是视频合成了。
首先我们弄清楚视频合成的思路:
视频合成的方式,实际上是用过IO流,按照顺序把每一个文件的内容写到输出文件中去。
创建MediaMuxer:
// 创建MediaMuxer
val mediaMuxer = MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
mediaMuxer.setOrientationHint(90)
之所以要旋转90度,因为摄像头本身的视频是旋转了90度,所以设置90度,是为了摆正视频的方向。
视频文件有音频和视频两种内容,我们要把他们单独提取出来,然后写到输出文件中的音轨和视轨上,那么我们需要知道他们的基本信息:
// 找到文件的视频格式和音频格式
var findAudioFormat = false
var findVideoFormat = false
var audioFormat: MediaFormat? = null
var videoFormat: MediaFormat? = null
for (file in videoList) {
val mediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(file.absolutePath)
if (!findAudioFormat) {
// 找到文件中的音频格式
audioFormat = findFormat(mediaExtractor, "audio/")
findAudioFormat = audioFormat != null
}
if (!findVideoFormat) {
// 找到文件中的视频格式
videoFormat = findFormat(mediaExtractor, "video/")
Log.e("lzp", videoFormat.toString())
findVideoFormat = videoFormat != null
}
mediaExtractor.release()
if (findAudioFormat && findVideoFormat) {
break
}
}
private fun findFormat(mediaExtractor: MediaExtractor, prefix: String): MediaFormat? {
for (i in 0 until mediaExtractor.trackCount) {
val format = mediaExtractor.getTrackFormat(i)
val mime = format.getString("mime")
if (mime.startsWith(prefix)) {
return format
}
}
return null
}
通过以上代码,我们知道了文件中音频和视频的格式,然后我们需要在输出文件中创建这两种格式的轨道:
var mediaMuxerAudioTrackIndex = 0
// 合成文件添加指定格式的音轨
if (findAudioFormat) {
mediaMuxerAudioTrackIndex = mediaMuxer.addTrack(audioFormat!!)
}
// 合成文件添加指定格式的视轨
var mediaMuxerVideoTrackIndex = 0
if (findVideoFormat) {
mediaMuxerVideoTrackIndex = mediaMuxer.addTrack(videoFormat!!)
}
// 开始合成
mediaMuxer.start()
通过mediaMuxer.addTrack()方法,我们在输出文件中创建好了音轨和视轨,接下来就是按顺序把文件中的内容写进去了。
遍历文件列表中的每一个文件,找到对应的音轨和视轨:
// 文件的音轨
val audioMediaExtractor = MediaExtractor()
audioMediaExtractor.setDataSource(file.absolutePath)
val audioTrackIndex = findTrackIndex(audioMediaExtractor, "audio/")
if (audioTrackIndex >= 0) {
audioMediaExtractor.selectTrack(audioTrackIndex)
hasAudio = true
}
// 文件的视轨
val videoMediaExtractor = MediaExtractor()
videoMediaExtractor.setDataSource(file.absolutePath)
val videoTrackIndex = findTrackIndex(videoMediaExtractor, "video/")
if (videoTrackIndex >= 0) {
videoMediaExtractor.selectTrack(videoTrackIndex)
hasVideo = true
}
// 如果音频视频都没有,直接跳过该文件
if (!hasAudio && !hasVideo) {
audioMediaExtractor.release()
videoMediaExtractor.release()
continue
}
上面的代码跟一开始找到音频视频格式差不多,唯一的差别是调用了 audioMediaExtractor.selectTrack(audioTrackIndex),目的是选中文件中指定的轨道,之后我们读取数据的时候,得到的只是这个轨道中的数据。
接下来是读取和写入的常规操作,我们以音频为例:
// 写入音轨
if (hasAudio) {
var hasDone = false
var lastPts = 0L
while (!hasDone) {
mReadBuffer.rewind()
// 读取音轨的数据
val frameSize = audioMediaExtractor.readSampleData(mReadBuffer, 0)
// 数据已经读取完毕
if (frameSize < 0) {
hasDone = true
} else {
// 这里使用了MediaCodec.BufferInfo(),保存要写入的数据的信息
val bufferInfo = MediaCodec.BufferInfo()
bufferInfo.offset = 0
bufferInfo.size = frameSize
// 数据的时间戳,一定要对齐,否则可能会出现音视频不同步的情况
bufferInfo.presentationTimeUs = audioPts + audioMediaExtractor.sampleTime
// 判断是否是关键帧
if ((audioMediaExtractor.sampleFlags and MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
}
mReadBuffer.rewind()
// 写到到合成文件中
mediaMuxer.writeSampleData(mediaMuxerAudioTrackIndex, mReadBuffer, bufferInfo)
// 更新提取的位置,下一次读取新的内容
audioMediaExtractor.advance()
// 时间戳的对齐
if (audioMediaExtractor.sampleTime > 0) {
lastPts = audioMediaExtractor.sampleTime
}
}
}
audioPts += lastPts
// 使用结束释放资源
audioMediaExtractor.release()
}
视频的写入也是同样的流程,这里就不多做介绍了。
最后别忘了释放资源:
mediaMuxer.stop()
mediaMuxer.release()
总结
这次的案例非常的简单,我们弄懂了MediaExtractor和MediaMuxer的基本使用,并了解了视频合成的基本流程,我们的目的就达到了。其他的问题大家可以参考一下Demo。
网友评论