一. 功能简介
调用Android Camera组件,获取预览时的byte[]数组,之后渲染到Activity的TextureView中,同时采用MediaCodec进行AVC(即H264)编码,使用MediaMuxer进行打包,生成MP4文件。
二. 架构设计
整个功能模块分为如下几个子功能:
- 相机组件的使用(权限申请、预览画面的获取、尺寸设置等等,暂时不包括对焦,因为主要是编码功能)
- TextureView的使用(将预览画面渲染到屏幕上)
- MediaCodec的使用(MediaFormat的选择、bufferQueue等等)
- MediaMuxer的使用(混合器,混合H264视频码流和音频码流,音频码流暂时还没加入,后期有时间再加入)
三. 相机组件
这里采用的是Android.Hardware.Camera
类,注意区分Android.graphic.Camera
和Android.Hardware.Camera2
,前者是用于3D图形绘制的工具,而后者是新的Camera操作类,这里选择的是第一代的Camera。
首先最重要的一件事就是在清单中,申请权限。
拿到权限后,我们需要对 Camera进行初始化:
主要是初始化:cameraId和outputSizes属性,前者是相机的ID,后者是相机输出的画幅尺寸。
private fun initCamera() {
//初始化相机的一些参数
val instanceOfCameraUtil = CameraUtils.getInstance(this).apply {
this@CameraActivity.cameraManager = this.cameraManager!!
cameraId = this.getCameraId(false)!! //默认使用后置相机
//获取指定相机的输出尺寸列表
outPutSizes = this.getCameraOutputSizes(cameraId, SurfaceTexture::class.java)!!.get(0)
}
}
假定此时,你的Layout文件中,已经还有一个TextureView(id:textureView),我们需要声明一个TextureView.SurfaceTextureListener
:
private val mSurfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
openCameraPreview(surface, width, height)
}
}
我们需要关注的是第四个重写方法,该方法将在TextureView可用时,被回调,这时,我们就可以根据该方法来构建预览画面了,这部分的代码在网络上很多的帖子中都有做过叙述。需要注意的是,这里并不包含画面对焦等等功能,如果有需要可以自行百度一下。
四. 预览画面的构建
一开始我的设想是构建一个手机竖屏视频全屏播放器,那么(横纵)尺寸一定是:1080 * 1920。这样一来,我们输入编码器的长宽分别是:1080 * 1920,但是,我们在setPreviewCallback
获得的照片数据:byte[]数组中,我们的照片是横着摆放的,这样一来,尺寸就变成了:1920 * 1080。这个数据直接送入编码器会导致画面的异常:
所以,这个一维的byte[]数组中存放的nv21数据,我们需要将它对应的位置给旋转90度,这就是rotateYUV420Degree90
方法(方法参考文末的【附】)
private fun openCameraPreview(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
//初始化预览尺寸,这些属性必须等到Texture可用后再回调,否则会出问题。
mPreviewSize = Size(1080, 1920) //初始化编码器,强制声明成1080*1920,也可以根据这的长宽来定,1080P是一个比较通用的尺寸,但是放到全面屏中的全屏TextureView可能会导致画面拉伸等等问题,需要另外去解决。
mTextureView.setAspectRation(mPreviewSize.width, mPreviewSize.height);
mCameraDevice = Camera.open(0)
mCameraDevice.setDisplayOrientation(90)
/** * 获得捕获的视频信息。 /
mCameraDevice.parameters = mCameraDevice.parameters.apply {
this!!.setPreviewSize(mPreviewSize.height, mPreviewSize.width)
this.setPictureSize(mPreviewSize.height, mPreviewSize.width)
this.previewFormat = CAMERA_COLOR_FORMAT
}
/*
- Camera作为生产者,生产的图像数据,交给SurfaceTexture处理。
- 或者是进一步渲染
- 或者是显示,这里设置的PreviewTexture自然是显示。
- 这里的surfaceTexture实际上是当我们‘预览’TextureView可用的时候,被回调的这个回调函数中提供了一个钩子:surfaceTexture
- 这个surfaceTexure将会作为显示的载体,直接被显示出来。
*/
mCameraDevice.setPreviewTexture(surfaceTexture)
mCameraDevice.setPreviewCallback { data, camera ->
//注意:照片的宽高是反着的,曰,而不是日
if (::mHandler.isInitialized) {
mHandler.post {
//把横版视频分辨率:1920 * 1080 转换成竖版: 1080 * 1920
val verticalData = ImageFormatUtils.rotateYUV420Degree90(data, mPreviewSize.height,mPreviewSize.width)
onFrameAvailable(verticalData)
}
}
}
mCameraDevice.startPreview()
}
五. 编码器的声明
鉴于各种设备DSP芯片的区别,各种设备支持的色彩格式等等参数也有不同,在这里我就使用在小米10上高通865可用的色彩格式之一:COLOR_FormatYUV420SemiPlanar,即NV21,接下来,我们初始化MediaCodec
和MediaMuxer
。具体支持的格式需要真正运行时动态地去判断、获取。
如果设备的DSP芯片比较差,支持的格式也更少,硬解码是无法使用的,因此也应该适时地引入手段进行软件解码(FFmpeg等等)。这里仅例举MediaCodec的使用。格式必须配套,不配套的话会导致:色彩和位置之间的偏差、偏色、花屏等等各种问题。
private val MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVCprivate
val MEDIACODEC_COLOR_FORMAT = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar//接受的编NV21
private fun initEncoder() {
val supportedColorFormat = ImageFormatUtils.getSupportColorFormat()//获取支持的色彩格式
try {
mMediaCodec = MediaCodec.createEncoderByType(MEDIA_TYPE)
mMediaFormat = MediaFormat.createVideoFormat(MEDIA_TYPE,mPreviewSize.width,mPreviewSize.height).apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, MEDIACODEC_COLOR_FORMAT)//设置输入的颜色 I420,我们要先转换NV21成I420
setInteger(MediaFormat.KEY_BIT_RATE, 10000000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
}
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
//布置混合器
val fileName = this.obbDir.absolutePath + “/” + System.currentTimeMillis() + “.mp4” mMuxer = MediaMuxer(fileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) } catch (e: Exception) {
e.printStackTrace()
return
}
}
如果到Muxer没有出现错误,那么说明Codec和Muxer都构建成功了。
六. 数据的记录
我们需要开一个新的线程来作编码的记录,我们在Camera的预览界面拿到一帧数据后我们通过子线程的Handler,为其POST一个任务。
//编码线程
private lateinit var mHandler: Handler
private lateinit var mWorkerThread: HandlerThread
private fun startEncoder() {
isEncoding = true //开始编码
mMediaCodec.start() //构建连接器。
mWorkerThread = HandlerThread(“WorkerThread-Encoder”)
mWorkerThread.start()
mHandler = Handler(mWorkerThread.looper)
}
mCameraDevice.setPreviewCallback { data, camera ->
if (::mHandler.isInitialized) {
mHandler.post {
//把横版视频分辨率:1920 * 1080 转换成竖版: 1080 * 1920
val verticalData = ImageFormatUtils.rotateYUV420Degree90(data, mPreviewSize.height, mPreviewSize.width)
onFrameAvailable(verticalData)
}
}
}
我在查询Camera支持的分辨率的时候,发现所有的分辨率都是横版的分辨率,即:1920*1080版本的,但是我们MediaCodec最初设定的分辨率是竖版的,这里也是一个坑。
onFrameAvailable()方法中,我们不断地插入一个byte数组,这个数组中是相机实时传来的预览画面,我们对这个画面进行编码即可。编码完成后,将编码出来的画面接入到Muxer中:
文末
那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。点击我的GitHub下述资料免费领取
Android希望=技能+面试
-
技能
-
面试技巧+面试题
A%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md)**
Android希望=技能+面试 -
技能
[外链图片转存中…(img-CmkWHBD1-1646473129359)] -
面试技巧+面试题