音频采集:AudioRecord
VR自行车数据采集地址vip7.maltapi.com
视频采集:Camera预览回调YUV数据
编码:MediaCodec
合成封包MP4:MediaMuxer
首先确定几条线程处理任务
1.audioThread 音频采集和编码
2.videoThread 视频编码
3.muxerThread 合成
示例代码:Kotlin
所有详细代码已上传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)
}
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
相机参数设置
/**
* 初始化并打开相机,我这里默认打开的后置摄像头
*/
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()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
为避免文章过长,有些代码未贴出,可以直接到github查看,getAppropriatePreviewSizes(parameters)未贴出。
2.录像处理线程
录像的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, 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", "视频释放")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
初始化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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
视频编码(同步方式)
private fun encodeVideo(data: ByteArray, isFinish: Boolean) {
val videoArray = ByteArray(data.size)
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]
byteBuffer.clear()
byteBuffer.put(videoArray)
if (!isFinish) {
videoCodec!!.queueInputBuffer(index, 0, videoArray.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("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)
}
}
}
}
---------------------
作者:One_Month
来源:CSDN
原文:https://blog.csdn.net/One_Month/article/details/90765636
版权声明:本文为博主原创文章,转载请附上博文链接!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69931768/viewspace-2647506/ ,如需转载,请注明出处,否则将追究法律责任。