写在最后
最后我想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
相信它会给大家带来很多收获:
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
教程代码:【Github传送门】 |
---|
目录
一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染视频画面
- 3,OpenGL渲染多视频,实现画中画
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO数据缓冲区
- 6,Android音视频硬编码:生成一个MP4
三、Android FFmpeg音视频解码篇
- 1,FFmpeg so库编译
- 2,Android 引入FFmpeg
- 3,Android FFmpeg视频解码播放
- 4,Android FFmpeg+OpenSL ES音频解码播放
- 5,Android FFmpeg+OpenGL ES播放视频
- 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
- 7,Android FFmpeg视频编码
本文你可以了解到
本文主要简介Android使用硬解码API实现硬解码的流程,包含MediaCodec输入输出缓冲、MediaCodec解码流程、解码代码封装和讲解。
一、简介
MediaCodec 是Android 4.1(api 16)版本引入的编解码接口,同时支持音视频的编码和解码。
一定要好好理解接下来这两幅图,因为后续的代码就是基于这两幅图来编写的。
数据流
首先,来看看MediaCodec的数据流,也是官方Api文档中的,很多文章都会引用。
仔细看一下,MediaCodec将数据分为两部分,分别为input(左边)和output(右边),即输入和输出两个数据缓冲区。
input:是给客户端输入需要解码的数据(解码时)或者需要编码的数据(编码时)。
output:是输出解码好(解码时)或者编码好(编码时)的数据给客户端。
MediaCodec内部使用异步的方式对input和output数据进行处理。MediaCodec将处理好input的数据,填充到output缓冲区,交给客户端渲染或处理
注:客户端处理完数据后,必须手动释放output缓冲区,否则将会导致MediaCodec输出缓冲被占用,无法继续解码。
状态
依然是一副来自官方的状态图
再仔细看看这幅图,整体上分为三个大的状态:Sotpped、Executing、Released。
- Stoped:包含了3个小状态:Error、Uninitialized、Configured。
首先,新建MediaCodec后,会进入Uninitialized状态;
其次,调用configure方法配置参数后,会进入Configured;
- Executing:同样包含3个小状态:Flushed、Running、End of Stream。
再次,调用start方法后,MediaCodec进入Flushed状态;
接着,调用dequeueInputBuffer方法后,进入Running状态;
最后,当解码/编码结束时,进入End of Stream(EOF)状态。
这时,一个视频就处理完成了。
- Released:最后,如果想结束整个数据处理过程,可以调用release方法,释放所有的资源。
那么,Flushed是什么状态呢?
从图中我们可以看到,在Running或者End of Stream状态时,都可以调用flush方法,重新进入Flushed状态。
当我们在解码过程中,进入了End of Stream后,解码器就不再接收输入了,这时候,需要调用flush方法,重新进入接收数据状态。
或者,我们在播放视频过程中,想进行跳播,这时候,我们需要Seek到指定的时间点,这时候,也需要调用flush方法,清除缓冲,否则解码时间戳会混乱。
再次强调一下,一定要好好理解这两幅图,因为后续的代码就是基于这两幅图来编写的。
二、解码流程
MediaCodec有两种工作模式,分别为异步模式和同步模式,这里我们使用同步模式,异步模式可以参考官网例子。
根据官方的数据流图和状态图,画出一个最基础的解码流程如下:
经过初始化和配置以后,进入循环解码流程,不断的输入数据,然后获取解码完数据,最后渲染出来,直到所有数据解码完成(End of Stream)。
三、开始解码
根据上面的流程图,可以发现,无论音频还是视频,解码流程基本是一致的,不同的地方只在于【配置】、【渲染】两个部分。
定义解码器
因此,我们将整个解码流程抽象为一个解码基类:BaseDecoder,为了规范代码和更好的拓展性,我们先定义一个解码器:IDecoder,继承Runnable。
interface IDecoder: Runnable {
/**
- 暂停解码
*/
fun pause()
/**
- 继续解码
*/
fun goOn()
/**
- 停止解码
*/
fun stop()
/**
- 是否正在解码
*/
fun isDecoding(): Boolean
/**
- 是否正在快进
*/
fun isSeeking(): Boolean
/**
- 是否停止解码
*/
fun isStop(): Boolean
/**
- 设置状态监听器
*/
fun setStateListener(l: IDecoderStateListener?)
/**
- 获取视频宽
*/
fun getWidth(): Int
/**
- 获取视频高
*/
fun getHeight(): Int
/**
- 获取视频长度
*/
fun getDuration(): Long
/**
- 获取视频旋转角度
*/
fun getRotationAngle(): Int
/**
- 获取音视频对应的格式参数
*/
fun getMediaFormat(): MediaFormat?
/**
- 获取音视频对应的媒体轨道
*/
fun getTrack(): Int
/**
- 获取解码的文件路径
*/
fun getFilePath(): String
}
定义了解码器的一些基础操作,如暂停/继续/停止解码,获取视频的时长,视频的宽高,解码状态等等
为什么继承Runnable?
这里使用的是同步模式解码,需要不断循环压入和拉取数据,是一个耗时操作,因此,我们将解码器定义为一个Runnable,最后放到线程池中执行。
接着,继承IDecoder,定义基础解码器BaseDecoder。
首先来看下基础参数:
abstract class BaseDecoder: IDecoder {
//-------------线程相关------------------------
/**
- 解码器是否在运行
*/
private var mIsRunning = true
/**
- 线程等待锁
*/
private val mLock = Object()
/**
- 是否可以进入解码
*/
private var mReadyForDecode = false
//---------------解码相关-----------------------
/**
- 音视频解码器
*/
protected var mCodec: MediaCodec? = null
/**
- 音视频数据读取器
*/
protected var mExtractor: IExtractor? = null
/**
- 解码输入缓存区
*/
protected var mInputBuffers: Array? = null
/**
- 解码输出缓存区
*/
protected var mOutputBuffers: Array? = null
/**
- 解码数据信息
*/
private var mBufferInfo = MediaCodec.BufferInfo()
private var mState = DecodeState.STOP
private var mStateListener: IDecoderStateListener? = null
/**
- 流数据是否结束
*/
private var mIsEOS = false
protected var mVideoWidth = 0
protected var mVideoHeight = 0
//省略后面的方法
…
}
-
首先,我们定义了线程相关的资源,用于判断是否持续解码的mIsRunning,挂起线程的mLock等。
-
然后,就是解码相关的资源了,比如MdeiaCodec本身,输入输出缓冲,解码状态等等。
-
其中,有一个解码状态DecodeState和音视频数据读取器IExtractor。
定义解码状态
为了方便记录解码状态,这里使用一个枚举类表示
enum class DecodeState {
/*开始状态/
START,
/*解码中/
DECODING,
/*解码暂停/
PAUSE,
/*正在快进/
SEEKING,
/*解码完成/
FINISH,
/*解码器释放/
STOP
}
定义音视频数据分离器
前面说过,MediaCodec需要我们不断地喂数据给输入缓冲,那么数据从哪里来呢?肯定是音视频文件了,这里的IExtractor就是用来提取音视频文件中数据流。
Android自带有一个音视频数据读取器MediaExtractor,同样为了方便维护和拓展性,我们依然先定一个读取器IExtractor。
interface IExtractor {
/**
- 获取音视频格式参数
*/
fun getFormat(): MediaFormat?
/**
- 读取音视频数据
*/
fun readBuffer(byteBuffer: ByteBuffer): Int
/**
- 获取当前帧时间
*/
fun getCurrentTimestamp(): Long
/**
- Seek到指定位置,并返回实际帧的时间戳
*/
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/**
- 停止读取数据
*/
fun stop()
}
最重要的一个方法就是readBuffer,用于读取音视频数据流
定义解码流程
前面我们只贴出了解码器的参数部分,接下来,贴出最重要的部分,也就是解码流程部分。
abstract class BaseDecoder: IDecoder {
//省略参数定义部分,见上
…
final override fun run() {
mState = DecodeState.START
mStateListener?.decoderPrepare(this)
//【解码步骤:1. 初始化,并启动解码器】
if (!init()) return
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
waitDecode()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
//如果数据没有解码完毕,将数据推入解码器解码
if (!mIsEOS) {
//【解码步骤:2. 将数据压入解码器输入缓冲】
mIsEOS = pushBufferToDecoder()
}
//【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
val index = pullBufferFromDecoder()
if (index >= 0) {
//【解码步骤:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解码步骤:5. 释放输出缓冲】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解码步骤:6. 判断解码是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
//【解码步骤:7. 释放解码器】
release()
}
/**
- 解码线程进入等待
*/
private fun waitDecode() {
try {
if (mState == DecodeState.PAUSE) {
mStateListener?.decoderPause(this)
}
synchronized(mLock) {
mLock.wait()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
- 通知解码线程继续运行
*/
protected fun notifyDecode() {
synchronized(mLock) {
mLock.notifyAll()
}
if (mState == DecodeState.DECODING) {
mStateListener?.decoderRunning(this)
}
}
/**
- 渲染
*/
abstract fun render(outputBuffers: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo)
/**
- 结束解码
*/
abstract fun doneDecode()
}
最后
针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。**
[外链图片转存中…(img-eNrPbJoy-1715272455579)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!