经过体系API完结录制的几种计划与简略的运用
前语
关于怎么运用视频录制,之前也讲到过能够有多种办法完结,Intent 跳转体系页面,FFmpeg之类的软编,以及 CameraX 封装的硬编码完结,MediaRecorder 的装备硬编完结,也能够经过 MediaCodec + MediaMuxer 自行完结硬编。
由于讲到了三种 Camera 的运用预览以及简略的封装。那么本文就简略回顾一下后边几种硬编计划,都是 Android 体系 API 以及对其的封装 API。
文本并不涉及到太专业的音视频常识点。咱们只需求了解根本的录制视频需求的一些装备信息就能完结录制(毕竟体系的API现已封装的很完善了)。
- 帧率(Frame Rate): 帧率指的是每秒显现的图像数量,一般以 FPS 为单位,。帧率越高,视频中的动作就会显得愈加流畅。常见的帧率有 24、30、60 等。
- 分辨率(Resolution):分辨率是指视频的像素尺寸,一般由宽度和高度表示,如 1920×1080 或 1280×720。较高的分辨率能够供给更清晰的画面。
- 比特率(Bit Rate):比特率表示每秒传输的比特数,一般以 Mbps 为单位。比特率决议了视频的数据量,也影响了视频的质量和文件巨细,一般咱们都设置为分辨率的宽高乘以3或宽高乘以5。也能够设置一个比较大的值,例如3500000。
- 要害帧(I帧): 一般视频分为要害帧(I帧),猜测帧(P帧)和双向猜测帧(B帧),咱们当时只需求了解I帧是具有高质量和完整的图像信息,它们一般被用作要害帧。咱们一般挑选封面或缩略图都是从I帧这个独立画面帧中挑选。在录制中咱们一般需求挑选录制视频的I帧距离,一般都是选1(每一帧都成为要害帧,文件更大)或2(两个I帧之间会存在一些P帧或B帧,文件会更小)
简略了解这些之后咱们就能够开端录制了,那么咱们以哪一种 Camera 为例来完结硬编录制呢?
其实每一种 Camera 都有各自的优缺点,回调的数据不同,Camera1 回调的是 NV21 ,Camera2 与 Camerax 回调的是 YUV420 , 咱们能够经过一些工具类转化对应的格局,完结 Mediacodec 的编码 ,假如仅仅想简略的完结录制那么运用 CameraX 的 VideoCapture 用例可快速完结预览与录制功用。
用起来比较复杂的 Camera2 尽管代码完结较多,不同的设备兼容性和支撑度也各有不同,但它能够完结一些定制化需求。比例感光度,曝光,主动对焦,白平衡,多摄像头支撑等等。
本文所用到的录制 API 的完结,也是依据 Camera2 及其封装完结的。
下面具体讲一下不同的办法都是怎么完结的。
一、MediaRecorder 录制
本身运用 Intent 是很便利了,可是有些功用有兼容性问题,体系版本与设备支撑的程度也不同,导致并没有那么好用,除非啥都不限制。
所以早前的开发咱们都是依据体系给咱们封装的 MediaRecorder 来录制,经过装备选项就能够完结音频与视频的录制,能够说是十分的便利。
public void startCameraRecord() {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.reset();
if (mCamera != null) {
mMediaRecorder.setCamera(mCamera);
}
mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
@Override
public void onError(MediaRecorder mr, int what, int extra) {
if (mr != null) {
mr.reset();
}
}
});
mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); // 视频源
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 音频源
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 视频封装格局
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 音频格局
if (mBestPreviewSize != null) {
// mMediaRecorder.setVideoSize(mBestPreviewSize.width, mBestPreviewSize.height); // 设置分辨率
mMediaRecorder.setVideoSize(640, 480); // 设置分辨率
}
// mMediaRecorder.setVideoFrameRate(16); // 比特率
mMediaRecorder.setVideoEncodingBitRate(1024 * 512);// 设置帧频率,
mMediaRecorder.setOrientationHint(90);// 输出旋转90度,坚持竖屏录制
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);// 视频输出格局
mMediaRecorder.setOutputFile(mVecordFile.getAbsolutePath());
try {
mMediaRecorder.prepare();
mMediaRecorder.start();
} catch (IOException e) {
e.printStackTrace();
}
}
它是直接把相机的页面展现出来,不能对特效进行录制,无法指定编码源,只能是纯相机画面,而且对比特率分辨率的支撑不友好,需求适配设备支撑的分辨率等。
最让人难以接受的是许多机型发动 MediaRecorder 的有 ‘滴’ 的一声提示音,这个体系提示音真的是让人头秃。
难怪后边推出的 CameraX 的录制不用本身的 MediaRecorder ,而且 MediaRecorder 本身也是依据 MediaCodec 完结的,对其进行的封装,所以咱们能够看看更底层的 MediaCodec 怎么完结。
二、MediaCodec + Camera 异步完结视频编码
假如仅仅独自的视频画面录制,咱们不需求考虑音视频同步的问题,不需求处理时刻戳,咱们其实能够用 MediaCodec 异步回调的办法更简略的完结。
例如咱们能够把原始相机的 I420 格局直接编码为 H264 文件格局。
以 Camera2 为例,咱们之前的封装中咱们设置了回调,这儿就不重复写封装代码,有兴趣能够去前文检查,直接贴出运用的代码了。
//子线程中运用同步行列保存数据
private val originVideoDataList = LinkedBlockingQueue<ByteArray>()
//符号当时是否正在录制中
private var isRecording: Boolean = false
private lateinit var file: File
private lateinit var outputStream: FileOutputStream
fun setupCamera(activity: Activity, container: ViewGroup) {
file = File(CommUtils.getContext().externalCacheDir, "${System.currentTimeMillis()}-record.h264")
if (!file.exists()) {
file.createNewFile()
}
if (!file.isDirectory) {
outputStream = FileOutputStream(file, true)
}
val textureView = AspectTextureView(activity)
textureView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
mCamera2Provider = Camera2ImageReaderProvider(activity)
mCamera2Provider?.initTexture(textureView)
mCamera2Provider?.setCameraInfoListener(object :
BaseCommonCameraProvider.OnCameraInfoListener {
override fun getBestSize(outputSizes: Size?) {
mPreviewSize = outputSizes
}
override fun onFrameCannback(image: Image) {
if (isRecording) {
// 运用C库获取到I420格局,对应 COLOR_FormatYUV420Planar
val yuvFrame = yuvUtils.convertToI420(image)
// 与MediaFormat的编码格局宽高对应
val yuvFrameRotate = yuvUtils.rotate(yuvFrame, 90)
// 用于测验RGB图片的回调预览
bitmap = Bitmap.createBitmap(yuvFrameRotate.width, yuvFrameRotate.height, Bitmap.Config.ARGB_8888)
yuvUtils.yuv420ToArgb(yuvFrameRotate, bitmap!!)
mBitmapCallback?.invoke(bitmap)
// 旋转90度之后的I420格局增加到同步行列
val bytesFromImageAsType = yuvFrameRotate.asArray()
originVideoDataList.offer(bytesFromImageAsType)
}
}
override fun initEncode() {
mediaCodecEncodeToH264()
}
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture?, width: Int, height: Int) {
this@VideoH264RecoderUtils.surfaceTexture = surfaceTexture
}
})
container.addView(textureView)
}
当摄像头预备好的时分初始化编码器,在每一帧回调中,咱们增加到同步行列,由于编码与预览数据不是同一个线程。然后咱们能够运用异步回调的办法设置编码。
/**
* 预备数据编码成H264文件
*/
fun mediaCodecEncodeToH264() {
if (mPreviewSize == null) return
//确定要竖屏的,实在场景需求依据屏幕当时方向来判别,这儿简略写死为竖屏
val videoWidth: Int
val videoHeight: Int
if (mPreviewSize!!.width > mPreviewSize!!.height) {
videoWidth = mPreviewSize!!.height
videoHeight = mPreviewSize!!.width
} else {
videoWidth = mPreviewSize!!.width
videoHeight = mPreviewSize!!.height
}
YYLogUtils.w("MediaFormat的编码格局,宽:${videoWidth} 高:${videoHeight}")
//装备MediaFormat信息(指定H264格局)
val videoMediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight)
//增加编码需求的颜色格局
videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)
// videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
//设置帧率
videoMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
//设置比特率
videoMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mPreviewSize!!.width * mPreviewSize!!.height * 5)
//设置每秒要害帧距离
videoMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
//创立编码器
val videoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
//这儿采取的是异步回调的办法,与dequeueInputBuffer,queueInputBuffer 这样的办法获取数据有差异的
// 一种是异步办法,一种是同步的办法。
videoMediaCodec.setCallback(callback)
videoMediaCodec.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoMediaCodec.start()
}
/**
* 具体的音频编码Codec回调
*/
val callback = object : MediaCodec.Callback() {
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
Log.e("error", e.message ?: "Error")
}
/**
* 体系获取到有可用的输出buffer,写入到文件
*/
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
//获取outputBuffer
val outputBuffer = codec.getOutputBuffer(index)
val byteArray = ByteArray(info.size)
outputBuffer?.get(byteArray)
when (info.flags) {
MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> { //编码装备完结
// 创立一个指定巨细为 info.size 的空的 ByteArray 数组,数组内悉数元素被初始化为默认值0
configBytes = ByteArray(info.size)
// arraycopy仿制数组的办法,
// 5个参数,1.源数组 2.源数组的开端方位 3. 目标数组 4.目标组的开端方位 5. 要仿制的元素数量
// 这儿便是把装备信息悉数写入到configBytes数组,用于后期的编码
System.arraycopy(byteArray, 0, configBytes, 0, info.size)
}
MediaCodec.BUFFER_FLAG_END_OF_STREAM -> { //完结处理
//当悉数写完之后就回调出去
endBlock?.invoke(file.absolutePath)
}
MediaCodec.BUFFER_FLAG_KEY_FRAME -> { //包含要害帧(I帧),解码器需求这些帧才能正确解码视频序列
// 创立一个临时变量数组,指定巨细为 info.size + 装备信息 的空数组
val keyframe = ByteArray(info.size + configBytes!!.size)
// 先 copy 写入装备信息,悉数写完
System.arraycopy(configBytes, 0, keyframe, 0, configBytes!!.size)
// 再 copy 写入具体的帧数据,从装备信息的 end 索引开端写,悉数写完
System.arraycopy(byteArray, 0, keyframe, configBytes!!.size, byteArray.size)
//悉数写完之后咱们就能写入到文件中
outputStream.write(keyframe, 0, keyframe.size)
outputStream.flush()
}
else -> { //其他帧的处理
// 其他的数据不需求写入要害帧的装备信息,直接写入到文件即可
outputStream.write(byteArray)
outputStream.flush()
}
}
//开释
codec.releaseOutputBuffer(index, false)
}
/**
* 当体系有可用的输入buffer,读取同步行列中的数据
*/
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
val inputBuffer = codec.getInputBuffer(index)
val yuvData = originVideoDataList.poll()
//假如获取到自定义完毕符,优先完毕掉
if (yuvData != null && yuvData.size == 1 && yuvData[0] == (-333).toByte()) {
isEndTip = true
}
//正常的写入
if (yuvData != null && !isEndTip) {
inputBuffer?.put(yuvData)
codec.queueInputBuffer(
index, 0, yuvData.size,
surfaceTexture!!.timestamp / 1000, //注意这儿没有用体系时刻,用的是surfaceTexture的时刻
0
)
}
//假如没数据,写入空数据,等待履行...
if (yuvData == null && !isEndTip) {
codec.queueInputBuffer(
index, 0, 0,
surfaceTexture!!.timestamp / 1000, //注意这儿没有用体系时刻,用的是surfaceTexture的时刻
0
)
}
if (isEndTip) {
codec.queueInputBuffer(
index, 0, 0,
surfaceTexture!!.timestamp / 1000, //注意这儿没有用体系时刻,用的是surfaceTexture的时刻
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
}
}
callback 目标便是异步回调,每一行代码都做了具体的注释。
开端录制与完毕录制的操控:
/**
* 中止录制
*/
fun stopRecord(block: ((path: String) -> Unit)? = null) {
endBlock = block
//写入自定义的完毕符
originVideoDataList.offer(byteArrayOf((-333).toByte()))
isRecording = false
}
/**
* 开端录制
*/
fun startRecord() {
isRecording = true
}
录制的作用:
三、MediaCodec + AudioRecord 异步完结音频编码
当咱们运用 MediaCodec 完结了 H264 的录制之后,咱们以相同的办法能够独自的编译出音频,咱们以常见的格局 AAC 为例。
仅仅之前的视频源是来自 Camera2 的回调,而这儿咱们的音频源来自 AudioRecord 的录制。
//子线程中运用同步行列保存数据
private var mAudioList: LinkedBlockingDeque<ByteArray>? = LinkedBlockingDeque()
//符号当时是否正在录制中
private var isRecording: Boolean = false
// 输入的时分符号是否是完毕符号
private var isEndTip = false
/**
* 初始化音频收集
*/
private fun initAudioRecorder() {
//依据体系供给的办法核算最小缓冲区巨细
minBufferSize = AudioRecord.getMinBufferSize(
AudioConfig.SAMPLE_RATE,
AudioConfig.CHANNEL_CONFIG,
AudioConfig.AUDIO_FORMAT
)
//创立音频录制器目标
mAudioRecorder = AudioRecord(
MediaRecorder.AudioSource.MIC,
AudioConfig.SAMPLE_RATE,
AudioConfig.CHANNEL_CONFIG,
AudioConfig.AUDIO_FORMAT,
minBufferSize
)
file = File(CommUtils.getContext().externalCacheDir, "${System.currentTimeMillis()}-record.aac")
if (!file.exists()) {
file.createNewFile()
}
if (!file.isDirectory) {
outputStream = FileOutputStream(file, true)
bufferedOutputStream = BufferedOutputStream(outputStream, 4096)
}
YYLogUtils.w("终究输入的File文件为:" + file.absolutePath)
}
/**
* 发动音频录制
*/
fun startAudioRecord() {
//敞开线程发动录音
thread(priority = android.os.Process.THREAD_PRIORITY_URGENT_AUDIO) {
isRecording = true //符号是否在录制中
try {
//判别AudioRecord是否初始化成功
if (AudioRecord.STATE_INITIALIZED == mAudioRecorder.state) {
mAudioRecorder.startRecording() //音频录制器开端发动录制
val outputArray = ByteArray(minBufferSize)
while (isRecording) {
var readCode = mAudioRecorder.read(outputArray, 0, minBufferSize)
//这个readCode还有许多小于0的数字,表示某种过错,
if (readCode > 0) {
val realArray = ByteArray(readCode)
System.arraycopy(outputArray, 0, realArray, 0, readCode)
//将读取的数据保存到同步行列
mAudioList?.offer(realArray)
} else {
Log.d("AudioRecoderUtils", "获取音频原始数据的时分呈现了某些过错")
}
}
//自定义一个完毕符号符,便于让编码器识别是录制完毕
val stopArray = byteArrayOf((-666).toByte(), (-999).toByte())
//把自定义的符号符保存到同步行列
mAudioList?.offer(stopArray)
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
//开释资源
mAudioRecorder.release()
}
}
//测验编码
thread {
mediaCodecEncodeToAAC()
}
}
与视频的编码相同的是,都是在不同的线程进行数据收集与编码,所以仍是用同步行列来保存数据,咱们在子线程中敞开音频录制,一起敞开子线程发动异步回调办法的编码。
private fun mediaCodecEncodeToAAC() {
try {
//创立音频MediaFormat
val encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AudioConfig.SAMPLE_RATE, 1)
//装备比特率
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
//装备最大输入巨细
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize * 2)
//初始化编码器
mAudioMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
//这儿采取的是异步回调的办法,与dequeueInputBuffer,queueInputBuffer 这样的办法获取数据有差异的
// 一种是异步办法,一种是同步的办法。
mAudioMediaCodec?.setCallback(callback)
mAudioMediaCodec?.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
mAudioMediaCodec?.start()
} catch (e: IOException) {
Log.e("mistake", e.message ?: "Error")
} finally {
}
}
/**
* 具体的音频编码Codec回调
*/
val callback = object : MediaCodec.Callback() {
val currentTime = Date().time * 1000 //以微秒为核算单位
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
Log.e("error", e.message ?: "Error")
}
/**
* 体系获取到有可用的输出buffer,写入到文件
*/
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
//经过bufferinfo获取Buffer的数据,这些数据便是编码后的数据
val outBitsSize = info.size
//为AAC文件增加头部,头部占7字节
//AAC有 ADIF和ADTS两种 ADIF只要一个头部剩余都是音频文件
//ADTS是每一段编码都有一个头部
//outpacketSize是终究头部加上回来数据后的总巨细
val outPacketSize = outBitsSize + 7 // 7 is ADTS size
//依据index获取buffer
val outputBuffer = codec.getOutputBuffer(index)
// 防止buffer有offset导致自己从0开端获取,
// 取出数据(可是我实验的offset都为0,可能有些不为0的情况)
outputBuffer?.position(info.offset)
//设置buffer的操作上限方位,不清楚的能够查下ByteBuffer(NIO常识),
//了解limit ,position,clear(),filp()都是啥作用
outputBuffer?.limit(info.offset + outBitsSize)
//创立byte数组保存组合数据
val outData = ByteArray(outPacketSize)
//为数据增加头部,后边会贴出,便是在头部写入7个数据
addADTStoPacket(AudioConfig.SAMPLE_RATE, outData, outPacketSize)
//将buffer的数据存入数组中
outputBuffer?.get(outData, 7, outBitsSize)
outputBuffer?.position(info.offset)
//将数据写到文件
bufferedOutputStream.write(outData)
bufferedOutputStream.flush()
outputBuffer?.clear()
//开释输出buffer,并开释Buffer
codec.releaseOutputBuffer(index, false)
}
/**
* 当体系有可用的输入buffer,读取同步行列中的数据
*/
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
//依据index获取buffer
val inputBuffer = codec.getInputBuffer(index)
//从同步行列中获取还未编码的原始音频数据
val pop = mAudioList?.poll()
//判别是否到达音频数据的完毕,依据自定义的完毕符号符判别
if (pop != null && pop.size >= 2 && (pop[0] == (-666).toByte() && pop[1] == (-999).toByte())) {
//假如是完毕符号
isEndTip = true
}
//假如数据不为空,而且不是完毕符号,写入buffer,让MediaCodec去编码
if (pop != null && !isEndTip) {
//填入数据
inputBuffer?.clear()
inputBuffer?.limit(pop.size)
inputBuffer?.put(pop, 0, pop.size)
//将buffer还给MediaCodec,这个一定要还
//第四个参数为时刻戳,也便是,有必要是递加的,体系依据这个核算
//音频总时长和时刻距离
codec.queueInputBuffer(
index,
0,
pop.size,
Date().time * 1000 - currentTime,
0
)
}
// 由于2个线程谁先履行不确定,所以可能编码线程先发动,获取到行列的数据为null
// 而且也不是完毕数据,这个时分也要调用queueInputBuffer,将buffer换回去,写入
// 数据巨细就写0
// 假如为null就不调用queueInputBuffer 回调几次后就会导致无可用InputBuffer,
// 从而导致MediaCodec使命完毕 只能写个装备文件
if (pop == null && !isEndTip) {
codec.queueInputBuffer(
index,
0,
0,
Date().time * 1000 - currentTime,
0
)
}
//发现完毕标志,写入完毕标志,
//flag为MediaCodec.BUFFER_FLAG_END_OF_STREAM
//通知编码完毕
if (isEndTip) {
codec.queueInputBuffer(
index,
0,
0,
Date().time * 1000 - currentTime,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
endBlock?.invoke(file.absolutePath)
}
}
}
相同的处理,仅仅自定义完毕的标志符不同,而且由所以独自录制的音频咱们需求增加一个ADTS头才能正常的播放。网上随便抄一个:
private fun addADTStoPacket(sampleRateType: Int, packet: ByteArray, packetLen: Int) {
val profile = 2 // AAC LC
val chanCfg = 1 // CPE
packet[0] = 0xFF.toByte()
packet[1] = 0xF9.toByte()
packet[2] = ((profile - 1 shl 6) + (sampleRateType shl 2) + (chanCfg shr 2)).toByte()
packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
packet[4] = (packetLen and 0x7FF shr 3).toByte()
packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
packet[6] = 0xFC.toByte()
}
此刻咱们就能录制出音频文件。由于大部分都是固定的代码,仅仅装备不同,作用都是差不多了,每一行代码尽量都有具体的注释。
录制作用如下:
听不到?这没办法啦,自己去跑代码吧。
四、MediaCodec + MediaMuxer 同步音视频编码并封装格局
仅仅独自的音频与视频录制,咱们能够用回调的办法简略的处理了,那音视频一起录制成 MP4 格局呢?
我懂了,回调一个视频,回调一个音频,然后组合在一起就行了!
道理是这么个道理,可是不是这么完结的,音视频的编码有快慢,所以就会导致画面与音频不同步,假如想要保证音视频的同步,只要经过运用相同的时刻戳(timestamp)和呈现时刻戳(presentation timestamp),将编码后的音频和视频帧进行关联。
咱们一般都是运用同步的办法来编码更为便利,咱们以音频流的编码为一个线程,视频流的编码为一个视频,组成器 MediaMuxer 的操作放到一个线程中,然后各种完结各自的工作,终究输出MP4文件。
大致的步骤如下:
- 创立音频和视频的MediaCodec目标,并进行装备。
- 将待编码的音频数据供给给音频的MediaCodec进行编码,并获取编码后的音频帧。
- 将待编码的视频数据供给给视频的MediaCodec进行编码,并获取编码后的视频帧。
- 运用音频和视频帧的时刻戳和呈现时刻戳来坚持它们的对应关系。
- 将编码后的音频帧和视频帧写入到一个同享的输出缓冲区中。
- 运用MediaMuxer将同享的输出缓冲区中的音频和视频数据封装成终究的格局(例如MP4)。
- 完结音视频的编码和封装后,开释资源并完结操作。
视频的编码仍是之前的逻辑,从 Camera2 获取到数据源,然后增加给编码器处理。
private fun initVideoFormat() {
//确定要竖屏的,实在场景需求依据屏幕当时方向来判别,这儿简略写死为竖屏
val videoWidth: Int
val videoHeight: Int
if (mPreviewSize!!.width > mPreviewSize!!.height) {
videoWidth = mPreviewSize!!.height
videoHeight = mPreviewSize!!.width
} else {
videoWidth = mPreviewSize!!.width
videoHeight = mPreviewSize!!.height
}
YYLogUtils.w("MediaFormat的编码格局,宽:${videoWidth} 高:${videoHeight}")
//装备MediaFormat信息(指定H264格局)
val videoMediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight)
//增加编码需求的颜色类型
videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)
//设置帧率
videoMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
//设置比特率
videoMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mPreviewSize!!.width * mPreviewSize!!.height * 5)
//设置要害帧I帧距离
videoMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
videoCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
videoCodec!!.configure(videoMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoCodec!!.start()
}
/**
* 视频流的编码处理线程
*/
inner class VideoEncodeThread : Thread() {
//由于摄像头数据的获取与编译不是在同一个线程,仍是需求同步行列保存数据
private val videoData = LinkedBlockingQueue<ByteArray>()
// 用于Camera的回调中增加需求编译的原始数据,这儿应该为YNV420
fun addVideoData(byteArray: ByteArray?) {
videoData.offer(byteArray)
}
override fun run() {
super.run()
initVideoFormat()
while (!videoExit) {
// 从同步行列中取出 YNV420格局,直接编码为H264格局
val poll = videoData.poll()
if (poll != null) {
encodeVideo(poll, false)
}
}
//发送编码完毕标志
encodeVideo(ByteArray(0), true)
// 当编码完结之后,开释视频编码器
videoCodec?.release()
}
}
//调用Codec硬编音频为AAC格局
// dequeueInputBuffer 获取到索引,queueInputBuffer编码写入
private fun encodeVideo(data: ByteArray, isFinish: Boolean) {
val videoInputBuffers = videoCodec!!.inputBuffers
var videoOutputBuffers = videoCodec!!.outputBuffers
val index = videoCodec!!.dequeueInputBuffer(TIME_OUT_US)
if (index >= 0) {
val byteBuffer = videoInputBuffers[index]
byteBuffer.clear()
byteBuffer.put(data)
if (!isFinish) {
videoCodec!!.queueInputBuffer(index, 0, data.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("camera2", "编码video $index 写入buffer ${data.size}")
var dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
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]
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
muxerThread?.addVideoData(outputBuffer, bufferInfo)
}
Log.i(
"camera2",
"编码后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) {
dequeueIndex = videoCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
} else {
break
}
}
}
}
而音频的处理咱们能够不需求同步行列处理了,运用同步的编码直接在一个线程中处理即可。
inner class AudioEncodeThread : Thread() {
//由于音频运用同步的办法编译,且在同一个线程内,所以不需求额定运用同步行列来处理数据
// private val audioData = LinkedBlockingQueue<ByteArray>()
override fun run() {
super.run()
prepareAudioRecord()
}
}
/**
* 预备初始化AudioRecord
*/
private fun prepareAudioRecord() {
initAudioFormat()
// 初始化音频录制器
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) {
//拿到音频原始数据去编译为音频AAC文件
encodeAudio(byteArray, read, getPTSUs())
//继续读取音频原始数据,循环履行
read = read(byteArray, 0, SAMPLES_PER_FRAME)
}
// 当录制完结之后,开释录音器
audioRecorder?.release()
//发送EOS编码完毕信息
encodeAudio(ByteArray(0), 0, getPTSUs())
// 当编码完结之后,开释音频编码器
audioCodec?.release()
}
}
}
//调用Codec硬编音频为AAC格局
// dequeueInputBuffer 获取到索引,queueInputBuffer编码写入
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("camera2", "编码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(
"camera2",
"编码后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("camera2", "音频时刻戳 ${bufferInfo.presentationTimeUs / 1000}")
// bufferInfo.presentationTimeUs = getPTSUs()
val byteArray = ByteArray(bufferInfo.size + 7)
outputBuffer.get(byteArray, 7, bufferInfo.size)
addADTStoPacket(0x04, byteArray, bufferInfo.size + 7)
outputBuffer.clear()
val headBuffer = ByteBuffer.allocate(byteArray.size)
headBuffer.put(byteArray)
muxerThread?.addAudioData(outputBuffer, bufferInfo) //直接加入到封装线程了
// prevOutputPTSUs = bufferInfo.presentationTimeUs
}
audioCodec!!.releaseOutputBuffer(dequeueIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
dequeueIndex = audioCodec!!.dequeueOutputBuffer(bufferInfo, TIME_OUT_US)
} else {
break
}
}
}
}
private fun initAudioFormat() {
audioMediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AudioConfig.SAMPLE_RATE, 1)
audioMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
audioMediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
audioMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minSize * 2)
audioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
audioCodec!!.configure(audioMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
audioCodec!!.start()
}
private fun getPTSUs(): Long {
var result = System.nanoTime() / 1000L
if (result < prevOutputPTSUs)
result += prevOutputPTSUs - result
return result
}
/**
* 增加ADTS头
*/
private fun addADTStoPacket(sampleRateType: Int, packet: ByteArray, packetLen: Int) {
val profile = 2 // AAC LC
val chanCfg = 1 // CPE
packet[0] = 0xFF.toByte()
packet[1] = 0xF9.toByte()
packet[2] = ((profile - 1 shl 6) + (sampleRateType shl 2) + (chanCfg shr 2)).toByte()
packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
packet[4] = (packetLen and 0x7FF shr 3).toByte()
packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
packet[6] = 0xFC.toByte()
}
当咱们视频编码完结或者音频编码完结就能够把编码之后的数据增加到 MediaMuxer 的音频和视频数据缓冲中。
class MuxThread(val file: File) : Thread() {
//音频缓冲区
private val audioData = LinkedBlockingQueue<EncodeData>()
//视频缓冲区
private val videoData = LinkedBlockingQueue<EncodeData>()
companion object {
var muxIsReady = false
var videoMediaFormat: MediaFormat? = null
var audioMediaFormat: MediaFormat? = null
var muxExit = false
}
private lateinit var mediaMuxer: MediaMuxer
/**
* 需求先初始化Audio线程与资源,然后增加数据源到封装类中
*/
fun addAudioData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
audioData.offer(EncodeData(byteBuffer, bufferInfo))
}
/**
* 需求先初始化Video线程与资源,然后增加数据源到封装类中
*/
fun addVideoData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
videoData.offer(EncodeData(byteBuffer, bufferInfo))
}
private fun initMuxer() {
mediaMuxer = MediaMuxer(file.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
videoAddTrack = mediaMuxer.addTrack(videoMediaFormat!!)
audioAddTrack = mediaMuxer.addTrack(audioMediaFormat!!)
mediaMuxer.start()
muxIsReady = true
}
private fun muxerParamtersIsReady() = audioMediaFormat != null && videoMediaFormat != null
override fun run() {
super.run()
//校验音频编码与视频编码不为空
while (!muxerParamtersIsReady()) {
}
initMuxer()
while (!muxExit) {
if (audioAddTrack != -1) {
if (audioData.isNotEmpty()) {
val poll = audioData.poll()
Log.i("camera2", "混合写入audio音频 ${poll.bufferInfo.size} ")
mediaMuxer.writeSampleData(audioAddTrack, poll.buffer, poll.bufferInfo)
}
}
if (videoAddTrack != -1) {
if (videoData.isNotEmpty()) {
val poll = videoData.poll()
Log.i("camera2", "混合写入video视频 ${poll.bufferInfo.size} ")
mediaMuxer.writeSampleData(videoAddTrack, poll.buffer, poll.bufferInfo)
}
}
}
mediaMuxer.stop()
mediaMuxer.release()
Log.i("camera2", "组成器开释")
Log.i("camera2", "未写入audio音频 ${audioData.size}")
Log.i("camera2", "未写入video视频 ${videoData.size}")
}
}
当咱们敞开录制的时分,发动这些线程,此刻就会别离开端编码音频数据与视频数据,当音视频数据编码完结并各自绑定呈现时刻戳,终究增加到 MuxThread 中开端组成,组成的 MuxThread 内部持有终究的音视频数据,内部敞开遍历并判别是否有数据,开端组成为 MP4 文件,当中止播放的时分设置一个 Flag 变量,中止编码并输出文件。
作用:
【注意】这仅仅 Demo 等级仅仅用于演示 API 的运用,不要用于实际项目,内部许多 Bug 与兼容性问题,中止录制的时分是立马中止了比方录制10秒可是实际视频只要8秒,便是由于没有做中止的缓冲处理,而且其中资源开释等等并没有处理。假如想要自己写 MediaCodec + MediaMuxer 能够引荐看下面的 VideoCapture 的源码完结。
五、CameraX 自带视频录制
假如说上面咱们自己完结的同步硬编代码有这样或那样的问题,那我想要一个开箱即用的录制视频怎么办,其实 CameraX 的录制就现已够咱们用了,假如没有一些特效的需求,咱们运用 CameraX 的 VideoCapture 就完全能满意需求了!
假如按之前的用法,回调中自己编码,那么咱们就需求定义回调,拿到 Image 目标,然后经过自己写 MediaCodec + MediaMuxer 这样和上面的用法就没差异,一样的能够完结视频录制功用。
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
// 在每一帧上运用颜色矩阵
imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), new MyAnalyzer(mContext));
private class MyAnalyzer implements ImageAnalysis.Analyzer {
private YuvUtils yuvUtils = new YuvUtils();
public MyAnalyzer(Context context) {
}
@Override
public void analyze(@NonNull ImageProxy image) {
// 运用C库获取到I420格局,对应 COLOR_FormatYUV420Planar
YuvFrame yuvFrame = yuvUtils.convertToI420(image.getImage());
// 与MediaFormat的编码格局宽高对应
yuvFrame = yuvUtils.rotate(yuvFrame, 90);
// 旋转90度之后的I420格局增加到同步行列
videoThread.addVideoData(yuvFrame.asArray());
}
}
// 发动 AudioRecord 音频录制以及编码等逻辑
可是这一系列的编码操作,CameraX 现已帮咱们写好了录制视频的用例 VideoCapture ,它内部就现已帮咱们封装了 MediaCodec + MediaMuxer 的逻辑,咱们需求运用起来很简略:
//录制视频目标
mVideoCapture = VideoCapture.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setAudioRecordSource(MediaRecorder.AudioSource.MIC) //设置音频源麦克风
//视频帧率
.setVideoFrameRate(30)
//bit率
.setBitRate(3 * 1024 * 1024)
.build()
// 开端录制
fun startCameraRecord(outFile: File) {
mVideoCapture ?: return
val outputFileOptions: VideoCapture.OutputFileOptions = VideoCapture.OutputFileOptions.Builder(outFile).build()
mVideoCapture!!.startRecording(outputFileOptions, mExecutorService, object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
YYLogUtils.w("视频保存成功,outputFileResults:" + outputFileResults.savedUri)
mCameraCallback?.takeSuccess()
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
YYLogUtils.e(message)
}
})
}
当咱们装备完结终究就能运用这个用例开端录制视频,它内部完结和咱们的之前的办法不同,没有经过 I420 或 NV21 等格局编码,而是经过 Surface 直接编码,要害代码如下:
...
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
// 绑定到 Surface
Surface cameraSurface = mVideoEncoder.createInputSurface();
mCameraSurface = cameraSurface;
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
}
mDeferrableSurface = new ImmediateSurface(mCameraSurface);
mDeferrableSurface.getTerminationFuture().addListener(
cameraSurface::release, CameraXExecutors.mainThreadExecutor()
);
sessionConfigBuilder.addSurface(mDeferrableSurface);
在之前创立了一个输入Surface目标,并将其与视频编码器建立了关联。运用sessionConfigBuilder.addSurface()办法将 mDeferrableSurface 增加到会话装备中,以确保视频编码器运用这个 Surface 进行数据输入。这样,相机数据就能够经过该Surface输入到视频编码器中进行编码处理了。
源码在 androidx.camera.core.VideoCapture ,谷歌写的是很完善了,我个人也比较喜爱这种办法,运用 COLOR_FormatSurface 的办法兼容性与健壮性更好。
总结
本文大致上讲了一些 Android 本身供给的一些硬编码办法,本质都是 MediaCodec 和一些依据它封装的一些工具。包含 MediaRecorder 与 VideoCapture 都是依据它完结的。
本文的代码有一点多,咱们能够经过代码简略的了解 I420、NV21、Surface 几种数据源的不同编码办法。MediaCodec 不同的装备代表什么会有什么样的影响。咱们也能了解几种编码办法的不同用法,异步回调与同步处理有什么差异,封装组成器怎么运用?
对比几种数据源的编码办法,我个人比较喜爱 Surface 的办法(个人偏好),兼容性与后期的扩展性都会更好,包含后期咱们能够完结特效的预览与录制,能够直出录制的特效视频,都会更为便利。
需求注意的是,关于不同的 MediaCodec 的完结,本文中咱们运用了不同的办法,可是都是一些 API 的运用办法,没时刻完善,它的健壮性也并不好,我们能够用于参阅学习,并不引荐我们直接拿过去运用。真实引荐运用的反而是体系的 VideoCapture ,一些简略的录制作用用它完全够用了。(假如想要直接拿过去用的能够看看后边的文章)
已然 Camerax 中的 VideoCapture 这么好,那么能够经过它的录制视频办法在 Camera1 或 Camera2 上完结呢?
下一篇咱们就会完结一个自己的 VideoCapture 并以 Surface 源的办法录制视频,一起探讨一下特效预览与特效录制的完结与差异。
本文假如贴出的代码有不全的,能够点击源码打开项目进行检查,【传送门】。一起你也能够关注我的开源项目,后续一些改动与优化还有新功用都会继续更新。
我这么菜为什么会出这样的音视频文章?由于本人的岗位是运用开发,而业务需求一些轻度音视频录制相关的功用(能够参阅拼多多评论页面)。属于那种能够特效录制视频,也能够挑选本地视频进行处理,能够挑选去除原始音频加入自定义的背景音乐,能够增加简略的文本字幕或标签,进行简略的裁剪之类的功用,在2023年的今日来看,其实都现已算是运用开发的领域,并没有涉及专业的音视频常识(并没有剪映抖音快手那么NB的作用)。我本人其实并不太懂专业音视频常识也不是拿手这方面,假如本文的讲解有什么讹夺的当地,期望同学们一定要指出哦。有疑问也能够评论区交流学习进步,谢谢!
当然假如觉得本文还不错对你有些帮助的话,还请点赞
支撑一下哦,你的支撑是我最大的动力啦!
Ok,这一期就此完结。