Android音视频系列(四):使用MediaExtractor和MediaMuxer完成断点录制以及文件合成

前言

本来计划介绍一下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异常,一定要立刻查看一下源码注释,八成是调用顺序出了问题。

图片名称

非常简单的页面,我这里做了录制时间的检测,录到足够的时长,开始视频合成。我们先用MediaRecorder录制几个视频,如下图:

图片名称

我已经录制好了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。

Github下载地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值