- 音频采集:AudioRecord
- 视频采集:Camera预览回调YUV数据
- 编码:MediaCodec
- 合成封包MP4:MediaMuxer
首先确定几条线程处理任务
- audioThread 音频采集和编码
- videoThread 视频编码
- muxerThread 合成
示例代码:Kotlin
详细代码已上传github,有需要的朋友可以在评论里留言或私信老舅,示例Activity是Camera1PreviewActivity
代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。
在最后,会写出容易出现的问题,代码运行不正确的时候,可以对照下,是否 犯了这些错误
private fun initView() {
surfaceView = findViewById)
(object : Sur {
override fun surfaceRedrawNeeded(holder: SurfaceHolder?) {
}
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) {
isSurfaceAvailiable = true
this@Camera1PreviewAc = 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@Camera1PreviewAc = holder
thread {
//打开相机
openCamera)
}
}
})
}
2.相机参数设置
【更多音视频学习资料,点击下方链接免费领取↓↓,先码住不迷路~】
点击领取→音视频开发基础知识和资料包
/**
* 初始化并打开相机,我这里默认打开的后置摄像头
*/
private fun openCamera(cameraId: Int) {
mCamera = Camera.open(cameraId)
mCamera?.run {
setPreviewDisplay(holder)
setDisplayOrientation(this@Camera1PreviewActivity))
var cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(cameraId, cameraInfo)
Log.i("camera1", "相机方向 ${cameraIn}")
val parameters = parameters
parameters?.run {
//自动曝光结果给我爆一团黑,不能忍 自己设置
exposureCompensation = maxExposureCompensation
//自动白平衡
autoWhiteBalanceLock = isAutoWhiteBalanceLockSupported
//设置预览大小
appropriatePreviewSizes = getAppropriatePreviewSizes(parameters)
setPreviewSize(appropriatePreviewSizes?.width!!, appropriatePreviewSizes?.height!!)
//设置对焦模式
val supportedFocusModes = supportedFocusModes
if )) {
//设置自动对焦,启动自动对焦是通过Camera的autoFocus方法实现
//如果要连续对焦,这个方法要多次调用,这里就没有调用autoFocus
//想要连续对焦的可以自己实现,通过Handler连续发送消息就行
focusMode = Camera.Parame
}
previewFormat = ImageFormat.NV21
}
//相机资源回收的时候,注意setPreviewCallBack(null),将回调移除
setPreviewCallback { data, camera ->
//isRecording是一个开启录制的标志,回调帧数据存放在集合中等待编码器编码
if (isRecording) {
if (data != null) {
Log.i("camera1", "获取视频数据 ${da}")
Log.i("camera1", "视频线程是否为 $videoThread")
videoT(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) {
videoDa(byteArray)
}
override fun run() {
()
//创建编码用的MediaFormat,下面贴出
initVideoFormat()
//创建视频编码器MediaCodec
videoCodec = MediaCodec.createEncoderByType)
videoCodec!!.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoCodec!!.start()
//如果未设置结束,就循环编码数据
while (!videoExit) {
val poll = videoDa()
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,
MediaCodecIn
)
//设置帧率
videoMediaFormat.setInteger, 30)
//设置比特率
videoMediaFormat.setInteger(
MediaFormat.KEY_BIT_RATE,
appropriatePreviewSizes!!.width * appropriatePreviewSizes!!.height * 5
)
//设置每秒关键帧间隔
videoMediaFormat.setInteger, 5)
}
视频编码(同步方式)
【更多音视频学习资料,点击下方链接免费领取↓↓,先码住不迷路~】
点击领取→音视频开发基础知识和资料包
private fun encodeVideo(data: ByteArray, isFinish: Boolean) {
val videoArray = ByteArray(da)
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]
by()
by(videoArray)
if (!isFinish) {
videoCodec!!.queueInputBuffer(index, 0, videoArray.size, Sy()/1000, 0)
} else {
videoCodec!!.queueInputBuffer(
index,
0,
0,
Sy()/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 == null)
MuxT = videoCodec!!.outputFormat
}
if (dequeueIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
videoOutputBuffers = videoCodec!!.outputBuffers
}
while (dequeueIndex >= 0) {
val outputBuffer = videoOutputBuffers[dequeueIndex]
//由于配置性信息在之前的MediaFormat已经包含,这里就不需要写入MediaMuxer了
if and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bu = 0
}
//将编码数据加入队列等待Muxer写入
if (bu != 0) {
muxerThread?.addVideoData(outputBuffer, bufferInfo)
}
Log.i(
"camera1",
"编码后video $dequeueIndex bu ${bu} bu ${ou()}"
)
videoCodec!!.releaseOutputBuffer(dequeueIndex, false)
//检查是否结束
if 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) {
audioDa(byteArray)
}
override fun run() {
()
prepareAudioRecord()
}
}
/**
* 准备初始化AudioRecord
*/
private fun prepareAudioRecord() {
initAudioFormat()
audioCodec = MediaCodec.createEncoderByType)
audioCodec!!.configure(audioMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
audioCodec!!.start()
//创建audiorecord对象,配置文件都在AudioCongfig中,minsize是根据系统方法算出,请查看github
audioRecorder = AudioRecord(
MediaRecorder.AudioSource.MIC, AudioCon,
AudioCon, AudioCon, 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]
by()
by(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 == null) {
MuxT = 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 bu ${bu} bu ${ou()}"
)
if and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bu = 0
}
if (bu != 0) {
Log.i("camera1","音频时间戳 ${bu /1000}")
muxerThread?.addAudioData(outputBuffer, bufferInfo)
}
audioCodec!!.releaseOutputBuffer(dequeueIndex, false)
if 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) {
audioDa(EncodeData(byteBuffer, bufferInfo))
}
fun addVideoData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
videoDa(EncodeData(byteBuffer, bufferInfo))
}
private fun initMuxer() {
val file = File, "muxer.mp4")
if (!()) {
()
}
mediaMuxer = MediaMuxer(
,
MediaMuxer.Ou
)
audioAddTrack = mediaMuxer.addTrack(audioMediaFormat)
videoAddTrack = mediaMuxer.addTrack(videoMediaFormat)
//注意添加轨道,必须在start之前进行
mediaMuxer.start()
muxIsReady = true
}
private fun muxerParamtersIsReady() = audioMediaFormat != null && videoMediaFormat != null
override fun run() {
()
//判断音视频MediaFormat是否都获取到了
while (!muxerParamtersIsReady()) {
}
//初始化,添加音视频轨道,开启合成
initMuxer()
Log.i("camera1", "当前记录状态 $isRecording ")
while (!muxExit) {
if (audioAddTrack != -1) {
if ()) {
val poll = audioDa()
Log.i("camera1", "混合写入音频 ${poll.bu} ")
mediaMuxer.writeSampleData(audioAddTrack, , Info)
}
}
if (videoAddTrack != -1) {
if ()) {
val poll = videoDa()
Log.i("camera1", "混合写入视频 ${poll.bu} ")
mediaMuxer.writeSampleData(videoAddTrack, , Info)
}
}
}
//写入完成,释放
mediaMuxer.stop()
mediaMuxer.release()
Log.i("camera1", "合成器释放")
Log.i("camera1", "未写入音频 ${audioDa}")
Log.i("camera1", "未写入视频 ${videoDa}")
}
}
这些就是这个系列的主要过程,下面写几点要注意的地方,也是容易造成程序出错的地方
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可以通过 获取,预览大小设置成系统不支持的,录制视频 很可能出现问题
如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!