音频采集:AudioRecord
视频采集:Camera预览回调YUV数据
编码:MediaCodec
合成封包MP4:MediaMuxer
首先确定几条线程处理任务
1.audioThread 音频采集和编码
2.videoThread 视频编码
3.muxerThread 合成
示例代码:Kotlin
所有详细代码已上传github,后面会给出地址,示例Activity是Camera1PreviewActivity
代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。
在最后,会写出容易出现的问题,代码运行不正确的时候,可以对照下,是否
犯了这些错误
1.初始化和打开相机
预览界面用的SurfaceView,通过前面的学习应该知道相机预览,就不多说
private fun initView() {
surfaceView = findViewById(com.example.mediastudyproject.R.id.surface_view)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback2 {
override fun surfaceRedrawNeeded(holder: SurfaceHolder?) {
}
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) {
isSurfaceAvailiable = true
[email protected] = holder
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
isSurfaceAvailiable = false
mCamera?.stopPreview()
//这里要把之前设置的预览回调取消,不然关闭app,camera释放了,但是还在回调,会报异常
mCamera?.setPreviewCallback(null)
mCamera?.release()
mCamera = null
}
override fun surfaceCreated(holder: SurfaceHolder?) {
isSurfaceAvailiable = true
[email protected] = holder
thread {
//打开相机
openCamera(Camera.CameraInfo.CAMERA_FACING_BACK)
}
}
})
}
相机参数设置
/**
* 初始化并打开相机,我这里默认打开的后置摄像头
*/
private fun openCamera(cameraId: Int) {
mCamera = Camera.open(cameraId)
mCamera?.run {
setPreviewDisplay(holder)
setDisplayOrientation(WindowDegree.getDegree(this@Camera1PreviewActivity))
var cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(cameraId, cameraInfo)
Log.i("camera1", "相机方向 ${cameraInfo.orientation}")
val parameters = parameters
parameters?.run {
//自动曝光结果给我爆一团黑,不能忍 自己设置
exposureCompensation = maxExposureCompensation
//自动白平衡
autoWhiteBalanceLock = isAutoWhiteBalanceLockSupported
//设置预览大小
appropriatePreviewSizes = getAppropriatePreviewSizes(parameters)
setPreviewSize(appropriatePreviewSizes?.width!!, appropriatePreviewSizes?.height!!)
//设置对焦模式
val supportedFocusModes = supportedFocusModes
if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
//设置自动对焦,启动自动对焦是通过Camera的autoFocus方法实现
//如果要连续对焦,这个方法要多次调用,这里就没有调用autoFocus
//想要连续对焦的可以自己实现,通过Handler连续发送消息就行
focusMode = Camera.Parameters.FOCUS_MODE_AUTO
}
previewFormat = ImageFormat.NV21
}
//相机资源回收的时候,注意setPreviewCallBack(null),将回调移除
setPreviewCallback { data, camera ->
//isRecording是一个开启录制的标志,回调帧数据存放在集合中等待编码器编码
if (isRecording) {
if (data != null) {
Log.i("camera1", "获取视频数据 ${data.size}")
Log.i("camera1", "视频线程是否为 $videoThread")
videoThread.addVideoData(data)
}
}
}
//开始预览
startPreview()
}
}
为避免文章过长,有些代码未贴出,可以直接到github查看,getAppropriatePreviewSizes(parameters)未贴出。
2.录像处理线程
录像的YUV数据设置的格式是NV21,Camera1的API可以返回这个,但是Camera2是不支持的,视频编码最好是NV12数据,最后要转换一下,录像线程主要做的是获取数据,转换成NV12 -> 编码为H264 ->写入Muxer
/**
*代码没有分离,直接在Activity创建的内部类,想要代码更简洁的可以分开
**/
inner class VideoEncodeThread : Thread() {
//预览的数据就直接添加到这个集合中
private val videoData = LinkedBlockingQueue<ByteArray>()
fun addVideoData(byteArray: ByteArray) {
videoData.offer(byteArray)
}
override fun run() {
super.run()
//创建编码用的MediaFormat,下面贴出
initVideoFormat()
//创建视频编码器MediaCodec
videoCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
videoCodec!!.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoCodec!!.start()
//如果未设置结束,就循环编码数据
while (!videoExit) {
val poll = videoData.poll()
if (poll != null) {
encodeVideo(poll, false)
}
}
//发送编码结束标志
encodeVideo(ByteArray(0), true)
//注意释放资源
videoCodec!!.release()
Log.i("camera1", "视频释放")
}
}
初始化MediaFormat
private fun initVideoFormat() {
videoMediaFormat =
MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
appropriatePreviewSizes!!.width,
appropriatePreviewSizes!!.height
)
//设置颜色类型 5.0新加的颜色格式
videoMediaFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
//设置帧率
videoMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
//设置比特率
videoMediaFormat.setInteger(
MediaFormat.KEY_BIT_RATE,
appropriatePreviewSizes!!.width * appropriatePreviewSizes!!.height * 5
)
//设置每秒关键帧间隔
videoMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
}
视频编码(同步方式)
private fun encodeVideo(data: ByteArray, isFinish: Boolean) {
val videoArray = ByteArray(data.size)
if (!isFinish) {
//NV21转NV12 网上找的,他两不同就是排列方式一个是VUVUVU一个是UVUVUV
//具体看github代码
NV21toI420SemiPlanar(
data,
videoArray,
appropriatePreviewSizes!!.width,
appropriatePreviewSizes!!.height
)
}
val videoInputBuffers = videoCodec!!.inputBuffers
var videoOutputBuffers = videoCodec!!.outputBuffers
//这个TIME_OUT_US设置的是0.01s也就是10000微秒,之前设置成1s,结果视频掉帧
//严重,声音也播放不了,说明这个值不能设置太大
val index = videoCodec!!.dequeueInputBuffer(TIME_OUT_US)
if (index >= 0) {
val byteBuffer = videoInputBuffers[index]
byteBuffer.clear()
byteBuffer.put(videoArray)
if (!isFinish) {
videoCodec!!.queueInputBuffer(index, 0, videoArray.size, System.nanoTime()/1000, 0)
} else {
videoCodec!!.queueInputBuffer(
index,
0,
0,
System.nanoTime()/1000,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
val bufferInfo = MediaCodec.BufferInfo()
Log.i("camera1", "编码video $index 写入buffer ${videoArray?.size}")
var dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
//这里需要注意,MediaMuxer要设置的音视频MediaFormat要在这里获取,设置过了就不用重新在更改
//如果不使用在这里获取的MediaFormat,极有可能最后MediaMuxer关闭时候出现关闭失败异常
if (dequeueIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
if (MuxThread.videoMediaFormat == null)
MuxThread.videoMediaFormat = videoCodec!!.outputFormat
}
if (dequeueIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
videoOutputBuffers = videoCodec!!.outputBuffers
}
while (dequeueIndex >= 0) {
val outputBuffer = videoOutputBuffers[dequeueIndex]
//由于配置性信息在之前的MediaFormat已经包含,这里就不需要写入MediaMuxer了
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
//将编码数据加入队列等待Muxer写入
if (bufferInfo.size != 0) {
muxerThread?.addVideoData(outputBuffer, bufferInfo)
}
Log.i(
"camera1",
"编码后video $dequeueIndex buffer.size ${bufferInfo.size} buff.position ${outputBuffer.position()}"
)
videoCodec!!.releaseOutputBuffer(dequeueIndex, false)
//检查是否结束
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
} else{
dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
}
}
}
}
3.音频线程
音频线程需要做2件事情,获取音频数据 -> 编码成AAC -> 准备写入Muxer,过程和视频
差不多,这里就不多解释步骤
准备AudioRecord录音
inner class AudioThread : Thread() {
private val audioData = LinkedBlockingQueue<ByteArray>()
fun addVideoData(byteArray: ByteArray) {
audioData.offer(byteArray)
}
override fun run() {
super.run()
prepareAudioRecord()
}
}
/**
* 准备初始化AudioRecord
*/
private fun prepareAudioRecord() {
initAudioFormat()
audioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
audioCodec!!.configure(audioMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
audioCodec!!.start()
//创建audiorecord对象,配置文件都在AudioCongfig中,minsize是根据系统方法算出,请查看github
audioRecorder = AudioRecord(
MediaRecorder.AudioSource.MIC, AudioConfig.SAMPLE_RATE,
AudioConfig.CHANNEL_CONFIG, AudioConfig.AUDIO_FORMAT, minSize
)
if (audioRecorder!!.state == AudioRecord.STATE_INITIALIZED) {
audioRecorder?.run {
startRecording()
val byteArray = ByteArray(SAMPLES_PER_FRAME)
var read = read(byteArray, 0, SAMPLES_PER_FRAME)
while (read > 0 && isRecording) {
Log.i("camera1", "读取到的音频 $read")
//音频数据的时间戳需要在读取的时候去获得,getPTSUs是获取当前系统纳秒表示时间
encodeAudio(byteArray, read, getPTSUs())
//读取的字节大小如果使用minSize,也就是计算得到的最小大小,编码合成后
//播放会没有声音,时间戳就不对,很可能这个大小的数据超过一帧数据大小,
//有待研究,1024和2048都能播放
read = read(byteArray, 0, SAMPLES_PER_FRAME)
}
audioRecorder!!.release()
//发送EOS编码结束信息
encodeAudio(ByteArray(0), 0, getPTSUs())
Log.i("camera1", "音频释放")
audioCodec!!.release()
}
}
}
音频编码(同步方式)
/***
* @param 音频数据个数
*/
private fun encodeAudio(audioArray: ByteArray?, read: Int, timeStamp: Long) {
val index = audioCodec!!.dequeueInputBuffer(TIME_OUT_US)
val audioInputBuffers = audioCodec!!.inputBuffers
if (index >= 0) {
val byteBuffer = audioInputBuffers[index]
byteBuffer.clear()
byteBuffer.put(audioArray, 0, read)
if (read != 0) {
audioCodec!!.queueInputBuffer(index, 0, read, timeStamp, 0)
} else {
audioCodec!!.queueInputBuffer(
index,
0,
read,
timeStamp,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
val bufferInfo = MediaCodec.BufferInfo()
Log.i("camera1", "编码audio $index 写入buffer ${audioArray?.size}")
var dequeueIndex = audioCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
if (dequeueIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
if (MuxThread.audioMediaFormat == null) {
MuxThread.audioMediaFormat = audioCodec!!.outputFormat
}
}
var audioOutputBuffers = audioCodec!!.outputBuffers
if (dequeueIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
audioOutputBuffers = audioCodec!!.outputBuffers
}
while (dequeueIndex >= 0) {
val outputBuffer = audioOutputBuffers[dequeueIndex]
Log.i(
"camera1",
"编码后audio $dequeueIndex buffer.size ${bufferInfo.size} buff.position ${outputBuffer.position()}"
)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
Log.i("camera1","音频时间戳 ${bufferInfo.presentationTimeUs /1000}")
muxerThread?.addAudioData(outputBuffer, bufferInfo)
}
audioCodec!!.releaseOutputBuffer(dequeueIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
} else {
dequeueIndex = audioCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
}
}
}
}
过程和视频编码基本一致
4.MediaMuxer合成线程
MediaMuxer的线程我单独提出来了,创建了一个类,他的任务就是
创建MediaMuxer对象 -> 获取音视频MediaFormat来添加音视频轨道 -> 开启合成 ->
获取集合数据,写入
class MuxThread(val context: Context) : Thread() {
private val audioData = LinkedBlockingQueue<EncodeData>()
private val videoData = LinkedBlockingQueue<EncodeData>()
companion object {
var muxIsReady = false
var audioMediaFormat: MediaFormat? = null
var videoMediaFormat: MediaFormat? = null
var muxExit = false
}
private lateinit var mediaMuxer: MediaMuxer
fun addAudioData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
audioData.offer(EncodeData(byteBuffer, bufferInfo))
}
fun addVideoData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
videoData.offer(EncodeData(byteBuffer, bufferInfo))
}
private fun initMuxer() {
val file = File(context.filesDir, "muxer.mp4")
if (!file.exists()) {
file.createNewFile()
}
mediaMuxer = MediaMuxer(
file.path,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
)
audioAddTrack = mediaMuxer.addTrack(audioMediaFormat)
videoAddTrack = mediaMuxer.addTrack(videoMediaFormat)
//注意添加轨道,必须在start之前进行
mediaMuxer.start()
muxIsReady = true
}
private fun muxerParamtersIsReady() = audioMediaFormat != null && videoMediaFormat != null
override fun run() {
super.run()
//判断音视频MediaFormat是否都获取到了
while (!muxerParamtersIsReady()) {
}
//初始化,添加音视频轨道,开启合成
initMuxer()
Log.i("camera1", "当前记录状态 $isRecording ")
while (!muxExit) {
if (audioAddTrack != -1) {
if (audioData.isNotEmpty()) {
val poll = audioData.poll()
Log.i("camera1", "混合写入音频 ${poll.bufferInfo.size} ")
mediaMuxer.writeSampleData(audioAddTrack, poll.buffer, poll.bufferInfo)
}
}
if (videoAddTrack != -1) {
if (videoData.isNotEmpty()) {
val poll = videoData.poll()
Log.i("camera1", "混合写入视频 ${poll.bufferInfo.size} ")
mediaMuxer.writeSampleData(videoAddTrack, poll.buffer, poll.bufferInfo)
}
}
}
//写入完成,释放
mediaMuxer.stop()
mediaMuxer.release()
Log.i("camera1", "合成器释放")
Log.i("camera1", "未写入音频 ${audioData.size}")
Log.i("camera1", "未写入视频 ${videoData.size}")
}
}
这些就是这个系列的主要过程,下面写几点要注意的地方,也是容易造成程序出错的地方
1.音频录制和编码,设置的读取大小不能使用计算得到的最小大小,不然会出现播放
没有声音,使用1024或者2048字节编码一次能够得到正确结果
2.MediaCodec编码,获取可用Buffer等待时间不能太大,不然会出现编码后视频跳帧
严重,音频也没有声音
3.MediaMuxer获取到的MediaFormat最好是在MediaCodec编码过程中,通过上述代
码呈现的方式获得,不然可能出现missing specific data,关闭MediaMuxer失败异常
4.MediaMuxer的添加音视频轨道,必须在start之前完成
5.Camera设置的setPreviewCallback在释放Camera资源的时候,也要把它释放,通过
setPreviewCallback(null),不然会报Camera仍在被使用,在Camera调用release之后
的异常
6.设置到预览数据大小,必须是系统给定的,系统支持的大小,Camera1可以通过
parameters.getSupportedPreviewSizes获取,预览大小设置成系统不支持的,录制视频
很可能出现问题
项目github地址,代码在Camera1PreviewActivity