您的位置:首页 > 财经 > 金融 > 鞍山云网站_上海人才中心_竞价托管哪家便宜_搭建网站需要哪些步骤

鞍山云网站_上海人才中心_竞价托管哪家便宜_搭建网站需要哪些步骤

2024/12/22 1:25:01 来源:https://blog.csdn.net/ZLGSPACE/article/details/144149356  浏览:    关键词:鞍山云网站_上海人才中心_竞价托管哪家便宜_搭建网站需要哪些步骤
鞍山云网站_上海人才中心_竞价托管哪家便宜_搭建网站需要哪些步骤

前言

本篇博文主要讲述的是基于Android原生MediaCodec通过Camera2 API进行图像数据采集并编码为H.264的实现过程,如果对此感兴趣的不妨驻足观看,也欢迎大家大家对本文中描述不当或者不正确的地方进行指正。如果对于Camera2预览还不熟悉的可以观看博主上一篇博文:Android 基于Camera2 API进行摄像机图像预览。

MediaCodec简介

MediaCodec 主要是用于对音视频进行编解码。它通常与 MediaExtractor、MediaMuxer、Surface 和 AudioTrack 等组件一起使用。MediaCodec 支持硬件加速,可以利用设备的硬件资源来提高编解码的性能。

1、MediaCodec 编解码流程

MediaCodec 采用异步方式处理数据,使用一组输入输出缓冲区(ByteBuffer)。编解码流程大致如下:

  • 请求一个空的输入缓冲区,填充满数据后传递给 MediaCodec 处理。

  • MediaCodec 处理完数据后,将结果输出至一个空的输出缓冲区中。

  • 从 MediaCodec 获取输出缓冲区的数据,消耗掉里面的数据后,释放回编解码器。

具体流程可以参考下图:

在这里插入图片描述

2、MediaCodec 生命周期

在这里插入图片描述

从上图可以看出MediaCodec 的生命周期包括三种状态:Stopped、Executing、Released。

  • Stopped,分三种子状态:

    • Configured,MediaCodec实例创建后,调用configure方法后就进入了Configured状态

    • Uninitialized,MediaCodec实例被创建后,在调用configure方法前都处于该状态;

    • Error,MediaCodec遇到错误时进入该状态,通常可能是队列操作返回错误或异常导致的;

  • Executing,分三种状态

    • Flushed,在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。可以在Executing状态的任何时候通过调用flush()方法返回到Flushed子状态;

    • Running,一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。通过调用stop()方法转移到Uninitialized状态;

    • End of Stream,将一个带有end-of-stream标记的输入buffer入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入buffer,但它仍然产生输出buffer直到end-of-stream标记输出

  • Released,当使用完MediaCodec后,必须调用release()方法释放其资源。调用 release()方法进入最终的Released状态;

编码实现

老规矩,依然需要对MediaCodec进行封装,在封装之前我们需要先设计下对应的接口,而根据上面了解到的MediaCodec生命周期中的几个状态我们需要对其进行记录,方便对外获取,因此接口设计如下:

interface ICodec {enum class State{IDLE,START,STOP,CLOSE}fun start()fun stop()fun close()fun getState():State
}interface IVideoEncoder: ICodec 

这里的close对应的是MediaCodec的release接口。ICodec设计思想是考虑到后续会扩展出不止VideocEncoder还会有Decoder因此只包含了最原始的几个接口设计,针对Encoder和Decoder区别性的接口可以基于ICodec进行扩展继续添加,因为IVideoEncoder暂时没有需要额外添加的接口所以只是单纯的继承自ICodec,后面有需要再添加即可。

接着让我们再编写一个VideoCodec类并实现IVideoEncoder接口,开始我们的编码实现。

class VideoEncoder(private var params:VideoEncParams = VideoEncParams()):IVideoEncoder {private var state = ICodec.State.IDLEprivate var enccoder:MediaCodec? = nullprivate var inputSurface:Surface? = nullprivate var encodeThread:Thread? = nullprivate var callback:EncoderCallback? = nullfun setEncoderCallback(callback:EncoderCallback){this.callback = callback}interface EncoderCallback{fun onCallback(data:ByteArray,frameFlags:Int)}

VideoEncoder实现了IVideoEncoder接口并且构造时需要传入VideoEncParams,VideoEncParams我们等下再看,inputSurface是编码输入源,encodeThread用于异步进行编码,callback则是对外提供编码后数据的回调,回调并不仅仅只包含编码后的帧数据,还包含有一个Int类型的frameFlags,这个frameFlags可以理解为编码这一帧的类型,例如SPS帧或者关键帧等,现在我们回过头来看下传入的VideoEncParams:

class VideoEncParams(var mime:String = "video/avc",var codecWidth:Int = 1920,var codecHeight:Int = 1080,var bitRate:Int = 2048,var fps:Int = 30,var keyInterval:Float = 1f,var rotation:Int = 90
)

参数貌似有点多,但实际编码过程中的传参根据需要可能会更多,只是我这里罗列的这些参数已经满足我们当前示例的需要了,这些参数的意义等下在配置到编码器的时候我们再详细解释,继续往下。

    override fun start() {if(state == ICodec.State.START) returnstate =  ICodec.State.STARTif(enccoder == null){enccoder = MediaCodec.createEncoderByType(params.mime).apply {configure(createMediaFormat(),null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)}inputSurface = enccoder?.createInputSurface()}enccoder?.start()encodeThread = Thread(encodeTask).apply { start() }}

这里我们实现了IVideoEncoder的第一个接口函数,也就是我们启动编码器的接口,函数的第一行代码做了一个保护,防止多次调用同一个编码器的start,如果全局变量enccoder为null就通过MediaCodec.createEncoderByType函数进行创建,创建时需要传入一个String类型的参数,这里我们传入的是params中保存的mime,上面我们也看到了这里的mime是“video/avc”。之后调用编码器configure函数进行了初始化配置,通过上面MediaCodec的生命周期可知,MediaCodec在编解码之前必须要先通过configure函数进行配置,才可以正常使用。

而这里的configure需要传入的参数有点多,我们一个一个看:

第一个参数类型是MediaFormat,createMediaFormat()函数的实际作用就是这里对VideoEncParams进行了转换将其转换成了MediaFormat对象。

第二个参数类型为Surface,是设置解码器的显示Surface我们这里用的是编码器,因此直接设置为null。

第三个参数类型为MediaCrypto,主要是与媒体数据加解密有关,我们这边不需要因此也设置为null。

第四个参数类型为int,设置为MediaCodec.CONFIGURE_FLAG_ENCODE配置MediaCodec为编码器。

现在我们来看下createMediaFormat()函数实现。

    private fun  createMediaFormat(): MediaFormat {var mediaFormat =  MediaFormat.createVideoFormat(params.mime,params.codecWidth,params.codecHeight)//设置比特率mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,params.bitRate)//设置帧率mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,params.fps)//设置颜色模式mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)//设置关键帧频率 帧/秒mediaFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL,params.keyInterval)//设置摄像机旋转角度mediaFormat.setInteger(MediaFormat.KEY_ROTATION,params.rotation)LogPrint.debug("createMediaFormat mediaFormat:$mediaFormat")return mediaFormat}

第一行代码是创建MediaFormat对象,MediaFormat可以理解为媒体数据的描述信息,通过createVideoFormat(),意思是创建一个与视频相关的描述信息对象,这里依然传入了mime,之后两个参数依次传入了需要编码视频的宽高。

MediaFormat设置媒体描述信息的方式是以键值对的方式传入的,键是字符串,值可以是int、long、float、String或ByteBuffer。

这里代码都有注释,就不再额外赘述。

让我们继续回到start()函数,当configure配置结束之后,通过enccoder获取到了一个Surface(inputSurface),该Surface可以理解为编码器的输入队列。

当编码器创建就绪之后,就创建了一个encodeThread线程,那么encodeTask的实现逻辑就是编码的关键逻辑了。

    private var encodeTask:Runnable = Runnable {//编码输出信息的对象,赋值由MediaCodec实现var bufferInfo = MediaCodec.BufferInfo()//当前可用的ByteBuffer索引var outputBufferIndex:Intvar encBuf:ByteArraytry {//死循环获取,也可以对MediaCodec设置callback获取while (state == ICodec.State.START) {//获取下一个可用的输出缓冲区,如果存在可用,返回值大于等于0,bufferInfo也会被赋值,最大等待时间是100000微秒,其实就是100毫秒outputBufferIndex = enccoder?.dequeueOutputBuffer(bufferInfo, 100000)!!//如果当前输出缓冲区没有可用的,返回负值,不同值含义不一样,有需要做判定即可if (outputBufferIndex < 0) {continue;}val outputBuffer: ByteBuffer = enccoder?.getOutputBuffer(outputBufferIndex)!!//调整数据位置,从offset开始。这样我们一会儿读取就不用传offset偏差值了。outputBuffer.position(bufferInfo.offset)//改完位置,那肯定要改极限位置吧,不然你数据不就少了数据末尾长度为offset的这一小部分?这两步不做也可以,get的时候传offset也一样outputBuffer.limit(bufferInfo.offset + bufferInfo.size)encBuf = ByteArray(bufferInfo.size)//获取编码数据outputBuffer.get(encBuf, 0, bufferInfo.size)callback?.onCallback(encBuf, bufferInfo.flags)//释放数据,不释放就一直在,MediaCodec数据满了可不行enccoder!!.releaseOutputBuffer(outputBufferIndex, false)}}catch (e:Exception){enccoder?.release()enccoder = nullinputSurface?.release()inputSurface = nullLog.e("VideoEncoder","VideoEncoder error:",e)}}

这块代码稍微有点多,我们来逐行看下,第一行代码是创建了一个BufferInfo对象,该对象主要是用来描述编码数据信息。第二行代码是当前MediaCodec输出buffer的索引位置,不理解的可以再回头看下上面给出的MediaCodec编解码流程图,第三行代码创建了一个ByteArray对象encBuf用于保存MediaCodec编码后的数据。

再往下就是核心的编码代码了,while循环中,通过MediaCodec的dequeueOutputBuffer获取当前可用的编码完成后的Buffer索引,第一个参数是上面我们创建的BufferInfo对象,第二个参数是等待时间,如果获取成功,MediaCodec会将缓存信息保存到BufferInfo对象中,并且返回Buffer的索引。

enccoder?.getOutputBuffer(outputBufferIndex)!!通过index获取输出Buffer。再之后是根据bufferInfo中的描述信息获取正确的缓存数据并保存到我们上面定义的encBuf中。

callback?.onCallback(encBuf, bufferInfo.flags)拿到编码数据之后通过该接口同步出去,这里这个代码可能有点不太合理,因为callback?.onCallback外部使用者如果使用不当可能会影响编码,理论上应该用队列同步出去会更好,这里我们先这样,后续我再进行优化。

while循环最后一行代码就是释放掉当前获取的输出Buffer数据,不释放的话MediaCodec缓存满了可能会出现异常。

再最后如果编码过程中出现异常,就会释放掉当前的编码器,按照生命周期其实通过MediaCodec.reset()也可以,当前这里为了方便,就直接释放了,等下次再start()重新创建即可。

至此编码就已经全部完成了,接下来让我们再加如一些其他的函数,使得VideoCodec更加完善。

    override fun stop() {if(state != ICodec.State.START) returnstate = ICodec.State.STOPencodeThread?.interrupt()enccoder?.stop()encodeThread = null}override fun close() {stop()state = ICodec.State.CLOSEenccoder?.release()inputSurface?.release()}override fun getState(): ICodec.State {return state}fun getInputSurface():Surface?{return inputSurface}

stop()和close()分别是停止解码器和释放解码器,stop()之后通过start()还能继续进行编码但是调用close()之后就不能再调用start(),否则就会报错,需要重新创建VideoEncodec进行编码。getState()没什么好说的,就只是返回当前编码器的状态,getInputSurface()将编码器的输入Surface(理解为队列)返回。

现在我们编码器就已经编写完成了,让我们看看如何跟我们的CameraWrapper进行组合编码Camera画面。让我们继续编写一个新的类CameraEncoder,通过这个类让我们把VideoEncodec和CameraWrapper组合起来,用以实现Camera画面编码为H.264。

class CameraEncoder :VideoEncoder.EncoderCallback{private var cameraWrapper: CameraWrapperprivate var videoEncoder:VideoEncoderprivate var callback:VideoEncoder.EncoderCallback? = nullconstructor(context:Context){cameraWrapper = CameraWrapper(context)videoEncoder = VideoEncoder()videoEncoder.setEncoderCallback(this)}fun start(surfaceView: SurfaceView){videoEncoder.start()cameraWrapper.setEncoderSurface(videoEncoder.getInputSurface()!!)cameraWrapper.startPreview(surfaceView)}fun stop(){videoEncoder.stop()cameraWrapper.stopPreview()cameraWrapper.setEncoderSurface(null)}fun close(){cameraWrapper.release()videoEncoder.close()}fun setEncoderCallback(callback:VideoEncoder.EncoderCallback){this.callback = callback}override fun onCallback(data: ByteArray,flags:Int) {callback?.onCallback(data,flags)}
}

因为代码量不多,而且没有什么难度,所以就直接全部贴出来了,在构造函数中同时创建了VideoCodec和CameraWrapper对象,对外的编码数据并不是通过VideoEncoder的回调直返回的,而是通过CameraEncoder的回调间接返回。

启动时先后启动了videoEncoder编码和cameraWrapper预览,特殊一点就是将videoEncoder中的输入Surface也就是我们上面说的可以理解为输入队列的哪个Surface传递到cameraWrapper这样在cameraWrapper预览时会自动关联起来。

停止时也是一样两个对象依次停止,不过在停止之后设置了cameraWrapper在启动时传入的Surface为空,其实也可以不用置空,但个人感觉这样可能会更好一点。

close()就不多说,与上面描述的start和stop逻辑一致。

至此我们Camera画面采集并编码为H.264就已经完成了。将编码后的数据保存到文件,然后通过VLC即可观看。

总结

Camera2与MediaCodec的结合在Android平台上提供了一种强大的视频处理解决方案。Camera2 API负责高效地从摄像头采集原始视频帧,而MediaCodec API则负责将这些帧实时编码为H.264格式,这是目前最广泛支持的视频编码标准之一。这种组合不仅利用了硬件加速来提高编码性能,减少CPU负担,还确保了视频的高质量输出和良好的兼容性。通过精确控制编码参数,可以根据应用需求调整视频的比特率、帧率和分辨率,实现定制化的视频录制和处理。总的来说,Camera2与MediaCodec的协同工作为开发者提供了一个灵活、高效的工具,用于创建和处理高质量的视频内容。

代码依然比较粗糙,但作为启蒙(用词貌似不当),应该是够了,后续随着博主的继续学习将会继续完善,感谢大家观看。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com