Android 音视频入门 (四)- 记录一次MediaCodec + MediaMuxer的使用,血与泪的总结

=======

调用Android Camera组件,获取预览时的byte[]数组,之后渲染到Activity的TextureView中,同时采用MediaCodec进行AVC(即H264)编码,使用MediaMuxer进行打包,生成MP4文件。

二. 架构设计

整个功能模块分为如下几个子功能:

  1. 相机组件的使用(权限申请、预览画面的获取、尺寸设置等等,暂时不包括对焦,因为主要是编码功能)
  2. TextureView的使用(将预览画面渲染到屏幕上)
  3. MediaCodec的使用(MediaFormat的选择、bufferQueue等等)
  4. MediaMuxer的使用(混合器,混合H264视频码流和音频码流,音频码流暂时还没加入,后期有时间再加入)

三. 相机组件

这里采用的是Android.Hardware.Camera类,注意区分Android.graphic.CameraAndroid.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。这个数据直接送入编码器会导致画面的异常:

1618745491334.png

所以,这个一维的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,接下来,我们初始化MediaCodecMediaMuxer。具体支持的格式需要真正运行时动态地去判断、获取。

如果设备的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都构建成功了。

一般通用的色彩格式是:I420,在这里使用的应该是COLOR_FormatYUV420Flexible这个变量。需要在数据编码前,将Nv21转换为I420的编码,如果不转换,使用主流的播放器也没有太大的问题。

六. 数据的记录

我们需要开一个新的线程来作编码的记录,我们在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)
}

注意,我们并不在此处就开启Muxer,我们会在子线程中接受数据的时候的某个状态开始进行混合。

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中:

private fun onFrameAvailable(_data: ByteArray?) {
if (!isEncoding) {
return;
}
//(可选NV21->I420),然后送入解码器
val data: ByteArray = _data!!

var index = 0
try {
index = mMediaCodec.dequeueInputBuffer(0)
} catch (e: Exception) {
e.printStackTrace()
return
}
if (index >= 0) {
val inputBuffer = mMediaCodec.getInputBuffer(index)
inputBuffer!!.clear()
inputBuffer.put(data, 0, data.size)
mMediaCodec.queueInputBuffer(
index,
0,
data.size,
System.nanoTime() / 1000,
0)
}
while (true) {
val bufferInfo = MediaCodec.BufferInfo()
val encoderStatus = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000)
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
break//稍后再试
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//输出的格式发生了改变,此处开启混合器
val newFormat = mMediaCodec.outputFormat
mVideoTrack = mMuxer!!.addTrack(newFormat)
mMuxer!!.start()
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//
} else {
//正常编码则获得缓冲区下标
val encodedDat = mMediaCodec.getOutputBuffer(encoderStatus)
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
//设置从XX地方开始读取数据
encodedDat!!.position(bufferInfo.offset)
//设置读数据总长度
encodedDat.limit(bufferInfo.offset + bufferInfo.size)
//写出MP4
if (!isEncoding) {
return
}
mMuxer!!.writeSampleData(mVideoTrack, encodedDat, bufferInfo)

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。

技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;

我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。

Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言

高级UI与自定义view;
自定义view,Android开发的基本功。

性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。

NDK开发;
未来的方向,高薪必会。

前沿技术;
组件化,热升级,热修复,框架设计

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多

当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。

不出半年,你就能看出变化!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多

当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。

不出半年,你就能看出变化!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的示例代码,演示如何使用MediaCodecMediaMuxer API剪切视频。请注意,这只是一个基本示例,实际上需要更多的代码来完成这个任务,例如处理不同的视频格式和编解码器参数等。 ```java import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.MediaMuxer; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; public class VideoClipper { private static final String TAG = "VideoClipper"; private static final String SAMPLE_PREFIX = "video_clip_"; private static final String SAMPLE_EXTENSION = ".mp4"; private static final int TIMEOUT_US = 10000; public static void clipVideo(String inputVideoPath, long startMs, long endMs) throws IOException { File inputFile = new File(inputVideoPath); MediaExtractor extractor = new MediaExtractor(); extractor.setDataSource(inputFile.toString()); int trackCount = extractor.getTrackCount(); int videoTrackIndex = -1; MediaFormat videoFormat = null; // Find the first video track index and its format for (int i = 0; i < trackCount; i++) { MediaFormat format = extractor.getTrackFormat(i); String mime = format.getString(MediaFormat.KEY_MIME); if (mime.startsWith("video/")) { videoTrackIndex = i; videoFormat = format; break; } } if (videoTrackIndex == -1) { throw new RuntimeException("No video track found in " + inputVideoPath); } // Configure the video codec MediaCodec videoDecoder = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME)); videoDecoder.configure(videoFormat, null, null, 0); videoDecoder.start(); // Configure the video muxer String outputVideoPath = new File(Environment.getExternalStorageDirectory(), SAMPLE_PREFIX + System.currentTimeMillis() + SAMPLE_EXTENSION).getAbsolutePath(); MediaMuxer muxer = new MediaMuxer(outputVideoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); // Copy the video header ByteBuffer header = ByteBuffer.allocate(1024); videoDecoder.getOutputFormat().getByteBuffer("csd-0").rewind(); header.put(videoDecoder.getOutputFormat().getByteBuffer("csd-0")); header.put(videoDecoder.getOutputFormat().getByteBuffer("csd-1")); header.flip(); int videoTrackIndexOut = muxer.addTrack(videoDecoder.getOutputFormat()); muxer.start(); // Extract video frames and write to muxer extractor.selectTrack(videoTrackIndex); ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); boolean inputDone = false; boolean outputDone = false; boolean videoDone = false; long videoStartTimeMs = -1; long videoEndTimeMs = -1; while (!outputDone) { if (!inputDone) { int inputIndex = videoDecoder.dequeueInputBuffer(TIMEOUT_US); if (inputIndex >= 0) { ByteBuffer inputBuffer = videoDecoder.getInputBuffer(inputIndex); int sampleSize = extractor.readSampleData(inputBuffer, 0); if (sampleSize < 0) { videoDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); inputDone = true; } else { long presentationTimeUs = extractor.getSampleTime(); if (videoStartTimeMs == -1) { videoStartTimeMs = presentationTimeUs / 1000; } if (videoEndTimeMs == -1 || presentationTimeUs / 1000 < videoEndTimeMs) { videoDecoder.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0); extractor.advance(); } else { inputDone = true; } } } } if (!videoDone) { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputIndex = videoDecoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); if (outputIndex >= 0) { if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { outputDone = true; } else { ByteBuffer outputBuffer = videoDecoder.getOutputBuffer(outputIndex); buffer.clear(); buffer.put(header); buffer.put(outputBuffer); buffer.flip(); muxer.writeSampleData(videoTrackIndexOut, buffer, bufferInfo); videoDecoder.releaseOutputBuffer(outputIndex, false); if (videoEndTimeMs == -1 && bufferInfo.presentationTimeUs / 1000 >= endMs) { videoEndTimeMs = bufferInfo.presentationTimeUs / 1000; videoDone = true; } } } } if (inputDone && videoDone) { outputDone = true; } } // Release resources extractor.release(); if (videoDecoder != null) { videoDecoder.stop(); videoDecoder.release(); } if (muxer != null) { muxer.stop(); muxer.release(); } Log.i(TAG, "Video clipped and saved to " + outputVideoPath); } } ``` 要使用这个示例代码,只需调用`clipVideo`方法并传入要剪切的视频文件路径、开始时间和结束时间。例如: ```java try { VideoClipper.clipVideo("/sdcard/input.mp4", 10000, 20000); } catch (IOException e) { e.printStackTrace(); } ``` 这将在SD卡根目录下创建一个名为“video_clip_*.mp4”的新视频文件,其中*是当前时间的毫秒级时间戳。这个新文件将是原始视频文件从10秒到20秒的剪辑版本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值