串联整个音视频录制过程,完成音视频的采集、编码、封包成 mp4 输出
发布时间:2023-03-23 13:42:36 所属栏目:教程 来源:
导读:所有详细代码已上传github,后面会给出地址,示例Activity是Camera1PreviewActivity
代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。
在最后,会写出容
代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。
在最后,会写出容
所有详细代码已上传github,后面会给出地址,示例Activity是Camera1PreviewActivity 代码中少了一些验证,比如设备支持预览的格式,这在之前的文章提到过,要注意自己的设备是否支持该设置。 在最后,会写出容易出现的问题,代码运行不正确的时候,可以对照下,是否犯了这些错误 1.初始化和打开相机 预览界面用的SurfaceView,通过前面的学习应该知道相机预览,就不多说 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, 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!!.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, 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, 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, SAMPLES_PER_FRAME) while (read > 0 && isRecording) { Log.i("camera1", "读取到的音频 $read") //音频数据的时间戳需要在读取的时候去获得,getPTSUs是获取当前系统纳秒表示时间 encodeAudio(byteArray, read, getPTSUs()) //读取的字节大小如果使用minSize,也就是计算得到的最小大小,编码合成后 //播放会没有声音,时间戳就不对,很可能这个大小的数据超过一帧数据大小, //有待研究,1024和2048都能播放 read = read(byteArray, SAMPLES_PER_FRAME) } audioRecorder!!.release() //发送EOS编码结束信息 encodeAudio(ByteArray(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, read) if (read != 0) { audioCodec!!.queueInputBuffer(index, timeStamp, 0) } else { audioCodec!!.queueInputBuffer( index, 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.bufferInfo) } } } //写入完成,释放 mediamuxer.stop() mediamuxer.release() Log.i("camera1", "合成器释放") Log.i("camera1", "未写入音频 ${audioData.size}") Log.i("camera1", "未写入视频 ${videoData.size}") } } (编辑:汽车网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐