• 音频采集:AudioRecord
  • 视频采集:Camera预览回调YUV数据
  • 编码:MediaCodec
  • 合成封包MP4:MediaMuxer

首先确定几条线程处理任务

  1. audioThread 音频采集和编码
  2. videoThread 视频编码
  3. muxerThread 合成

示例代码:Kotlin

详细代码已上传github,有需要的朋友可以在评论里留言或私信老舅,示例Activity是Camera1PreviewActivity

代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。

在最后,会写出容易出现的问题,代码运行不正确的时候,可以对照下,是否 犯了这些错误

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

this@Camera1PreviewActivity.holder = 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

this@Camera1PreviewActivity.holder = holder

thread {

//打开相机

openCamera(Camera.CameraInfo.CAMERA_FACING_BACK)

}

}

})

}

2.相机参数设置

【更多音视频学习资料,点击下方链接免费领取↓↓,先码住不迷路~】

点击领取→音视频开发基础知识和资料包

/**

* 初始化并打开相机,我这里默认打开的后置摄像头

*/

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)未贴出。

3.录像处理线程

录像的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)

}

}

}

}

4.音频线程

音频线程需要做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)

}

}

}

}

过程和视频编码基本一致

5.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获取,预览大小设置成系统不支持的,录制视频 很可能出现问题

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!