【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】6、Android音视频硬编码:生成一个MP4

【声 明】

首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。java

码字不易,转载请注明出处!git

教程代码:【Github传送门

目录

1、Android音视频硬解码篇:

2、使用OpenGL渲染视频画面篇

3、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视频编码

本文你能够了解到

本文将结合前面系列文中介绍的MediaCodec、OpenGL、EGL、FBO、MediaMuxer等知识,实现对一个视频的解码,编辑,编码,最后保存为新视频的流程。github

终于到了本篇章的最后一篇文章,前面的一系列文章中,围绕OpenGL,介绍了如何使用OpenGL来实现视频画面的渲染和显示,以及如何对视频画面进行编辑,有了以上基础之后,咱们确定想把编辑好的视频保存下来,实现整个编辑流程的闭环,本文就把最后一环补上。web

1、MediaCodec编码器封装

在【音视频硬解码流程:封装基础解码框架】这篇文章中,介绍了如何使用Android原生提供的硬编解码工具MediaCodec,对视频进行解码。同时,MediaCodec也能够实现对音视频的硬编码。segmentfault

仍是先来看看官方的编解码数据流图缓存

  • 解码流程

在解码的时候,经过 dequeueInputBuffer 查询到一个空闲的输入缓冲区,在经过 queueInputBuffer未解码 的数据压入解码器,最后,经过 dequeueOutputBuffer 获得 解码好 的数据。app

  • 编码流程

其实,编码流程和解码流程基本是同样的。不一样在于压入 dequeueInputBuffer 输入缓冲区的数据是 未编码 的数据, 经过 dequeueOutputBuffer 获得的是 编码好 的数据。框架

依葫芦画瓢,仿照封装解码器的流程,来封装一个基础编码器 BaseEncoderide

1. 定义编码器变量

完整代码请查看 BaseEncoder函数

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    private val TAG = "BaseEncoder"

    // 目标视频宽,只有视频编码的时候才有效
    protected val mWidth: Int = width

    // 目标视频高,只有视频编码的时候才有效
    protected val mHeight: Int = height

    // Mp4合成器
    private var mMuxer: MMuxer = muxer

    // 线程运行
    private var mRunning = true

    // 编码帧序列
    private var mFrames = mutableListOf<Frame>()

    // 编码器
    private lateinit var mCodec: MediaCodec

    // 当前编码帧信息
    private val mBufferInfo = MediaCodec.BufferInfo()

    // 编码输出缓冲区
    private var mOutputBuffers: Array<ByteBuffer>? = null

    // 编码输入缓冲区
    private var mInputBuffers: Array<ByteBuffer>? = null

    private var mLock = Object()

    // 是否编码结束
    private var mIsEOS = false

    // 编码状态监听器
    private var mStateListener: IEncodeStateListener? = null
    
    // ......
}
复制代码

首先,这是一个 abstract 抽象类,而且继承 Runnable ,上面先定义须要用到的内部变量。基本和解码相似。

要注意的是这里的宽高只对视频有效,MMuxer 是以前在【Mp4重打包】的是时候定义的Mp4封装工具。还有一个缓存队列mFrames,用来缓存须要编码的帧数据。

关于如何把数据写入到mp4中,本文再也不重述,请查看【Mp4重打包】。

其中一帧数据定义以下:

class Frame {
    //未编码数据
    var buffer: ByteBuffer? = null

    //未编码数据信息
    var bufferInfo = MediaCodec.BufferInfo()
    private set

    fun setBufferInfo(info: MediaCodec.BufferInfo) {
        bufferInfo.set(info.offset, info.size, info.presentationTimeUs, info.flags)
    }
}
复制代码

编码流程相对于解码流程来讲比较简单,分为3个步骤:

  • 初始化编码器
  • 将数据压入编码器
  • 从编码器取出数据,并压入mp4

2. 初始化编码器

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    //省略其余代码......
    
    init {
        initCodec()
    }
    
    /** * 初始化编码器 */
    private fun initCodec() {
        mCodec = MediaCodec.createEncoderByType(encodeType())
        configEncoder(mCodec)
        mCodec.start()
        mOutputBuffers = mCodec.outputBuffers
        mInputBuffers = mCodec.inputBuffers
    }
    
    
    /** * 编码类型 */
    abstract fun encodeType(): String

    /** * 子类配置编码器 */
    abstract fun configEncoder(codec: MediaCodec)
    
    // .......
}
复制代码

这里定义了两个虚函数,子类必须实现。一个用于配置音频和视频对应的编码类型,如视频编码为h264对应的编码类型为:"video/avc" ;音频编码为AAC对应的编码类型为:"audio/mp4a-latm"

根据获取到的编码类型,就能够初始化获得一个编码器。

接着,调用 configEncoder 在子类中配置具体的编码参数,这里暂不细说,定义音视频编码子类的时候再说。

2. 开启编码循环

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    // 省略其余代码......
    
    override fun run() {
        loopEncode()
        done()
    }
    
    /** * 循环编码 */
    private fun loopEncode() {
        while (mRunning && !mIsEOS) {
            val empty = synchronized(mFrames) {
                mFrames.isEmpty()
            }
            if (empty) {
                justWait()
            }
            if (mFrames.isNotEmpty()) {
                val frame = synchronized(mFrames) {
                    mFrames.removeAt(0)
                }

                if (encodeManually()) {
                    //【1. 数据压入编码】
                    encode(frame)
                } else if (frame.buffer == null) { // 若是是自动编码(好比视频),遇到结束帧的时候,直接结束掉
                    // This may only be used with encoders receiving input from a Surface
                    mCodec.signalEndOfInputStream()
                    mIsEOS = true
                }
            }
            //【2. 拉取编码好的数据】
            drain()
        }
    }
    
    // ......
}
复制代码

循环编码放在 Runnablerun 方法中。

loopEncode 中,将前面提到的 2(压数据)3(取数据) 合并在一块儿。逻辑也比较简单。

判断未编码的缓存队列是否为空,是则线程挂起,进入等待;不然编码数据,和取出数据。

有2点须要注意:

  • 音频和视频的编码流程稍微有点区别

音频编码 须要咱们本身将数据压入编码器,实现数据的编码。

视频编码 的时候,能够经过将 Surface 绑定给 OpenGL ,系统自动从 Surface 中取数据,实现自动编码。也就是说,不须要用户本身手动压入数据,只需从输出缓冲中取数据就能够了。

所以,这里定义一个虚函数,由子类控制是否须要手动压入数据,默认为true:手动压入。

下文中,将这两种形式分别叫作:手动编码自动编码

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {

    // 省略其余代码......
    
    /** * 是否手动编码 * 视频:false 音频:true * * 注:视频编码经过Surface,MediaCodec自动完成编码;音频数据须要用户本身压入编码缓冲区,完成编码 */
    open fun encodeManually() = true
    
    
    // ......
}
复制代码
  • 结束编码

在编码过程当中,若是发现 Framebuffernull ,就认为编码已经完成了,没有数据须要压入了。这时,有两种方法告诉编码器结束编码。

第一种,经过 queueInputBuffer 压入一个空数据,而且将数据类型标记设置为 MediaCodec.BUFFER_FLAG_END_OF_STREAM 。具体以下:

mCodec.queueInputBuffer(index, 0, 0,
    frame.bufferInfo.presentationTimeUs,
    MediaCodec.BUFFER_FLAG_END_OF_STREAM)
复制代码

第二种,经过 signalEndOfInputStream 发送结束信号。

咱们已经知道,视频是自动编码,因此没法经过第一种结束编码,只能经过第二种方式结束编码。

音频是手动编码,能够经过第一种方式结束编码。

一个坑
测试发现,视频结束编码的时候 signalEndOfInputStream 以后,在获取编码数据输出的时候,并无获得结束编码标记的数据,因此,上面的代码中,若是是自动编码,在判断到 Framebuffer 为空时,直接将 mIsEOF 设置为 true 了,退出了编码流程。

3. 手动编码

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其余代码......
    
    /** * 编码 */
    private fun encode(frame: Frame) {

        val index = mCodec.dequeueInputBuffer(-1)

        /*向编码器输入数据*/
        if (index >= 0) {
            val inputBuffer = mInputBuffers!![index]
            inputBuffer.clear()
            if (frame.buffer != null) {
                inputBuffer.put(frame.buffer)
            }
            if (frame.buffer == null || frame.bufferInfo.size <= 0) { // 小于等于0时,为音频结束符标记
                mCodec.queueInputBuffer(index, 0, 0,
                    frame.bufferInfo.presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            } else {
                mCodec.queueInputBuffer(index, 0, frame.bufferInfo.size,
                    frame.bufferInfo.presentationTimeUs, 0)
            }
            frame.buffer?.clear()
        }
    }
    
    // ......
}
复制代码

和解码同样,先查询到一个可用的输入缓冲索引,接着把数据压入输入缓冲。

这里,先判断是否结束编码,是则往输入缓冲压入编码结束标志

4. 拉取数据

把一帧数据压入编码器后,进入 drain 方法,顾名思义,咱们要把编码器输出缓冲中的数据,所有抽干。因此这里是一个while循环,直到输出缓冲没有数据 MediaCodec.INFO_TRY_AGAIN_LATER ,或者编码结束 MediaCodec.BUFFER_FLAG_END_OF_STREAM

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其余代码......
    
    /** * 榨干编码输出数据 */
    private fun drain() {
        loop@ while (!mIsEOS) {
            val index = mCodec.dequeueOutputBuffer(mBufferInfo, 0)
            when (index) {
                MediaCodec.INFO_TRY_AGAIN_LATER -> break@loop
                MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    addTrack(mMuxer, mCodec.outputFormat)
                }
                MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                    mOutputBuffers = mCodec.outputBuffers
                }
                else -> {
                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                        mIsEOS = true
                        mBufferInfo.set(0, 0, 0, mBufferInfo.flags)
                    }

                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                        // SPS or PPS, which should be passed by MediaFormat.
                        mCodec.releaseOutputBuffer(index, false)
                        continue@loop
                    }

                    if (!mIsEOS) {
                        writeData(mMuxer, mOutputBuffers!![index], mBufferInfo)
                    }
                    mCodec.releaseOutputBuffer(index, false)
                }
            }
        }
    }
    
    
    /** * 配置mp4音视频轨道 */
    abstract fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat)

    /** * 往mp4写入音视频数据 */
    abstract fun writeData(muxer: MMuxer, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
    
    // ......
}
复制代码

很重要的一点
mCodec.dequeueOutputBuffer 返回的是 MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 时,说明编码参数格式已经生成(好比视频的码率,帧率,SPS/PPS帧信息等),须要把这些信息写入到mp4对应媒体轨道中(这里经过 addTrack 在子类中配置音视频对应的编码格式),以后才能开始将编码完成的数据,经过MediaMuxer写入到相应媒体通道中。

5. 退出编码,释放资源

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其余代码......

    /** * 编码结束,是否资源 */
    private fun done() {
        try {
            release(mMuxer)
            mCodec.stop()
            mCodec.release()
            mRunning = false
            mStateListener?.encoderFinish(this)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    
    /** * 释放子类资源 */
    abstract fun release(muxer: MMuxer)
    
    // ......
}
复制代码

调用子类中的虚函数 release ,子类须要根据本身的媒体类型,释放对应mp4中的媒体通道。

6. 一些外部调用的方法

abstract class BaseEncoder(muxer: MMuxer, width: Int = -1, height: Int = -1) : Runnable {
    
    // 省略其余代码......
    
    /** * 将一帧数据压入队列,等待编码 */
    fun encodeOneFrame(frame: Frame) {
        synchronized(mFrames) {
            mFrames.add(frame)
            notifyGo()
        }
        // 延时一点时间,避免掉帧
        Thread.sleep(frameWaitTimeMs())
    }

    /** * 通知结束编码 */
    fun endOfStream() {
        Log.e("ccccc","endOfStream")
        synchronized(mFrames) {
            val frame = Frame()
            frame.buffer = null
            mFrames.add(frame)
            notifyGo()
        }
    }
    
    /** * 设置状态监听器 */
    fun setStateListener(l: IEncodeStateListener) {
        this.mStateListener = l
    }
    
    
    /** * 每一帧排队等待时间 */
    open fun frameWaitTimeMs() = 20L
    
    // ......
}

复制代码

这里有点须要注意,在把数据压入排队队列以后,作了一个默认 20ms 的延时,同时子类能够经过重写 frameWaitTimeMs 方法修改时间。

一个是为了不音频解码过快,致使数据堆积太多,音频在子类中从新设置等待为5ms,具体见子类 AudioEncoder 代码。

另外一个是由于因为视频是系统自动获取Surface数据,若是解码数据刷新太快,可能会致使漏帧,这里使用默认的20ms。

所以这里作了一个简单粗暴的延时,但并不是最好的解决方式

2、视频编码器

有了基础封装,写一个视频编码器还不是so easy的事吗?

反手就贴出一个视频编码器:

const val DEFAULT_ENCODE_FRAME_RATE = 30

class VideoEncoder(muxer: MMuxer, width: Int, height: Int): BaseEncoder(muxer, width, height) {

    private val TAG = "VideoEncoder"
    
    private var mSurface: Surface? = null

    override fun encodeType(): String {
        return "video/avc"
    }

    override fun configEncoder(codec: MediaCodec) {
        if (mWidth <= 0 || mHeight <= 0) {
            throw IllegalArgumentException("Encode width or height is invalid, width: $mWidth, height: $mHeight")
        }
        val bitrate = 3 * mWidth * mHeight
        val outputFormat = MediaFormat.createVideoFormat(encodeType(), mWidth, mHeight)
        outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
        outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, DEFAULT_ENCODE_FRAME_RATE)
        outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)

        try {
            configEncoderWithCQ(codec, outputFormat)
        } catch (e: Exception) {
            e.printStackTrace()
            // 捕获异常,设置为系统默认配置 BITRATE_MODE_VBR
            try {
                configEncoderWithVBR(codec, outputFormat)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "配置视频编码器失败")
            }
        }

        mSurface = codec.createInputSurface()
    }

    private fun configEncoderWithCQ(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 本部分手机不支持 BITRATE_MODE_CQ 模式,有可能会异常
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    private fun configEncoderWithVBR(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    override fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat) {
        muxer.addVideoTrack(mediaFormat)
    }

    override fun writeData( muxer: MMuxer, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo ) {
        muxer.writeVideoData(byteBuffer, bufferInfo)
    }

    override fun encodeManually(): Boolean {
        return false
    }

    override fun release(muxer: MMuxer) {
        muxer.releaseVideoTrack()
    }

    fun getEncodeSurface(): Surface? {
        return mSurface
    }
}
复制代码

继承了 BaseEncoder 实现全部的虚函数就能够了。

重点来看 configEncoder 这个方法。

i. 配置了码率 KEY_BIT_RATE

计算公式源自【MediaCodec编码OpenGL速度和清晰度均衡

Biterate = Width * Height * FrameRate * Factor 

Factor: 0.1~0.2
复制代码

ii. 配置帧率 KEY_FRAME_RATE ,这里为30帧/秒
iii. 配置关键帧出现频率 KEY_I_FRAME_INTERVAL ,这里为1帧/秒
iv. 配置数据来源 KEY_COLOR_FORMAT ,为 COLOR_FormatSurface,既来自 Surface
v. 配置码率模式 KEY_BITRATE_MODE

- BITRATE_MODE_CQ 忽略用户设置的码率,由编码器本身控制码率,并尽量保证画面清晰度和码率的均衡  
- BITRATE_MODE_CBR 不管视频的画面内容若是,尽量遵照用户设置的码率  
- BITRATE_MODE_VBR 尽量遵照用户设置的码率,可是会根据帧画面之间运动矢量  
(通俗理解就是帧与帧之间的画面变化程度)来动态调整码率,若是运动矢量较大,则在该时间段将码率调高,若是画面变换很小,则码率下降。 
复制代码

优先选择 BITRATE_MODE_CQ ,若是编码器不支持,切换回系统默认的 BITRATE_MODE_VBR

vi. 最后,经过编码器 codec.createInputSurface() 新建一个 Surface ,用于 EGL 的窗口绑定。视频解码获得的画面都将渲染到这个 Surface 中,MediaCodec自动从里面取出数据,并编码。

3、音频编码器

音频编码器则更加简单。

// 编码采样率率
val DEST_SAMPLE_RATE = 44100
// 编码码率
private val DEST_BIT_RATE = 128000

class AudioEncoder(muxer: MMuxer): BaseEncoder(muxer) {

    private val TAG = "AudioEncoder"

    override fun encodeType(): String {
        return "audio/mp4a-latm"
    }

    override fun configEncoder(codec: MediaCodec) {
        val audioFormat = MediaFormat.createAudioFormat(encodeType(), DEST_SAMPLE_RATE, 2)
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, DEST_BIT_RATE)
        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100*1024)
        try {
            configEncoderWithCQ(codec, audioFormat)
        } catch (e: Exception) {
            e.printStackTrace()
            try {
                configEncoderWithVBR(codec, audioFormat)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "配置音频编码器失败")
            }
        }
    }

    private fun configEncoderWithCQ(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 本部分手机不支持 BITRATE_MODE_CQ 模式,有可能会异常
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    private fun configEncoderWithVBR(codec: MediaCodec, outputFormat: MediaFormat) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            outputFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
            )
        }
        codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    }

    override fun addTrack(muxer: MMuxer, mediaFormat: MediaFormat) {
        muxer.addAudioTrack(mediaFormat)
    }

    override fun writeData( muxer: MMuxer, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo ) {
        muxer.writeAudioData(byteBuffer, bufferInfo)
    }

    override fun release(muxer: MMuxer) {
        muxer.releaseAudioTrack()
    }
}
复制代码

能够看到,configEncoder 实现也比较简单:

i. 设置音频比特率 MediaFormat.KEY_BIT_RATE,这里设置为 128000
ii. 设置输入缓冲区大小 KEY_MAX_INPUT_SIZE ,这里设置为 100*1024

4、整合

音频和视频的编码工具已经完成,接下来就来看看,如何把解码器、OpenGL、EGL、编码器串联起来,实现视频编辑功能。

  • 改造EGL渲染器

开始以前,须要改造一下【深刻了解OpenGL之EGL】 这篇文章中定义的EGL渲染器。

i. 在以前定义的渲染器中,只支持设置一个SurfaceView,并绑定到 EGL 显示窗口中。这里须要让它支持设置一个Surface,接收来自 VideoEncoder 中建立的Surface做为渲染窗口。

ii. 因为是要对窗口的画面进行编码,因此无需在渲染器中不断的刷新画面,只要在视频解码器解码出一帧的时候,刷新一下画面便可。同时把当前帧的时间戳传递给OpenGL。

完整代码以下,已经将新增的部分标记出来:

class CustomerGLRenderer : SurfaceHolder.Callback {

    private val mThread = RenderThread()

    private var mSurfaceView: WeakReference<SurfaceView>? = null

    private var mSurface: Surface? = null

    private val mDrawers = mutableListOf<IDrawer>()

    init {
        mThread.start()
    }

    fun setSurface(surface: SurfaceView) {
        mSurfaceView = WeakReference(surface)
        surface.holder.addCallback(this)

        surface.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener{
            override fun onViewDetachedFromWindow(v: View?) {
                stop()
            }

            override fun onViewAttachedToWindow(v: View?) {
            }
        })
    }

//-------------------新增部分-----------------

    // 新增设置Surface接口
    fun setSurface(surface: Surface, width: Int, height: Int) {
        mSurface = surface
        mThread.onSurfaceCreate()
        mThread.onSurfaceChange(width, height)
    }

    // 新增设置渲染模式 RenderMode见下面
    fun setRenderMode(mode: RenderMode) {
        mThread.setRenderMode(mode)
    }

    // 新增通知更新画面方法
    fun notifySwap(timeUs: Long) {
        mThread.notifySwap(timeUs)
    }
/----------------------------------------------

    fun addDrawer(drawer: IDrawer) {
        mDrawers.add(drawer)
    }

    fun stop() {
        mThread.onSurfaceStop()
        mSurface = null
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        mSurface = holder.surface
        mThread.onSurfaceCreate()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        mThread.onSurfaceChange(width, height)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        mThread.onSurfaceDestroy()
    }

    inner class RenderThread: Thread() {

        // 渲染状态
        private var mState = RenderState.NO_SURFACE

        private var mEGLSurface: EGLSurfaceHolder? = null

        // 是否绑定了EGLSurface
        private var mHaveBindEGLContext = false

        //是否已经新建过EGL上下文,用于判断是否须要生产新的纹理ID
        private var mNeverCreateEglContext = true

        private var mWidth = 0
        private var mHeight = 0

        private val mWaitLock = Object()

        private var mCurTimestamp = 0L

        private var mLastTimestamp = 0L

        private var mRenderMode = RenderMode.RENDER_WHEN_DIRTY

        private fun holdOn() {
            synchronized(mWaitLock) {
                mWaitLock.wait()
            }
        }

        private fun notifyGo() {
            synchronized(mWaitLock) {
                mWaitLock.notify()
            }
        }

        fun setRenderMode(mode: RenderMode) {
            mRenderMode = mode
        }

        fun onSurfaceCreate() {
            mState = RenderState.FRESH_SURFACE
            notifyGo()
        }

        fun onSurfaceChange(width: Int, height: Int) {
            mWidth = width
            mHeight = height
            mState = RenderState.SURFACE_CHANGE
            notifyGo()
        }

        fun onSurfaceDestroy() {
            mState = RenderState.SURFACE_DESTROY
            notifyGo()
        }

        fun onSurfaceStop() {
            mState = RenderState.STOP
            notifyGo()
        }

        fun notifySwap(timeUs: Long) {
            synchronized(mCurTimestamp) {
                mCurTimestamp = timeUs
            }
            notifyGo()
        }

        override fun run() {
            initEGL()
            while (true) {
                when (mState) {
                    RenderState.FRESH_SURFACE -> {
                        createEGLSurfaceFirst()
                        holdOn()
                    }
                    RenderState.SURFACE_CHANGE -> {
                        createEGLSurfaceFirst()
                        GLES20.glViewport(0, 0, mWidth, mHeight)
                        configWordSize()
                        mState = RenderState.RENDERING
                    }
                    RenderState.RENDERING -> {
                        render()
                        
                        //新增判断:若是是 `RENDER_WHEN_DIRTY` 模式,渲染后,把线程挂起,等待下一帧
                        if (mRenderMode == RenderMode.RENDER_WHEN_DIRTY) {
                            holdOn()
                        }
                    }
                    RenderState.SURFACE_DESTROY -> {
                        destroyEGLSurface()
                        mState = RenderState.NO_SURFACE
                    }
                    RenderState.STOP -> {
                        releaseEGL()
                        return
                    }
                    else -> {
                        holdOn()
                    }
                }
                if (mRenderMode == RenderMode.RENDER_CONTINUOUSLY) {
                    sleep(16)
                }
            }
        }

        private fun initEGL() {
            mEGLSurface = EGLSurfaceHolder()
            mEGLSurface?.init(null, EGL_RECORDABLE_ANDROID)
        }

        private fun createEGLSurfaceFirst() {
            if (!mHaveBindEGLContext) {
                mHaveBindEGLContext = true
                createEGLSurface()
                if (mNeverCreateEglContext) {
                    mNeverCreateEglContext = false
                    GLES20.glClearColor(0f, 0f, 0f, 0f)
                    //开启混合,即半透明
                    GLES20.glEnable(GLES20.GL_BLEND)
                    GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
                    generateTextureID()
                }
            }
        }

        private fun createEGLSurface() {
            mEGLSurface?.createEGLSurface(mSurface)
            mEGLSurface?.makeCurrent()
        }

        private fun generateTextureID() {
            val textureIds = OpenGLTools.createTextureIds(mDrawers.size)
            for ((idx, drawer) in mDrawers.withIndex()) {
                drawer.setTextureID(textureIds[idx])
            }
        }

        private fun configWordSize() {
            mDrawers.forEach { it.setWorldSize(mWidth, mHeight) }
        }

// ---------------------修改部分代码------------------------
        // 根据渲染模式和当前帧的时间戳判断是否须要从新刷新画面
        private fun render() {
            val render = if (mRenderMode == RenderMode.RENDER_CONTINUOUSLY) {
                true
            } else {
                synchronized(mCurTimestamp) {
                    if (mCurTimestamp > mLastTimestamp) {
                        mLastTimestamp = mCurTimestamp
                        true
                    } else {
                        false
                    }
                }
            }

            if (render) {
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
                mDrawers.forEach { it.draw() }
                mEGLSurface?.setTimestamp(mCurTimestamp)
                mEGLSurface?.swapBuffers()
            }
        }
        
//------------------------------------------------------

        private fun destroyEGLSurface() {
            mEGLSurface?.destroyEGLSurface()
            mHaveBindEGLContext = false
        }

        private fun releaseEGL() {
            mEGLSurface?.release()
        }
    }

    /** * 渲染状态 */
    enum class RenderState {
        NO_SURFACE, //没有有效的surface
        FRESH_SURFACE, //持有一个未初始化的新的surface
        SURFACE_CHANGE, //surface尺寸变化
        RENDERING, //初始化完毕,能够开始渲染
        SURFACE_DESTROY, //surface销毁
        STOP //中止绘制
    }

//---------新增渲染模式定义------------
    enum class RenderMode {
        // 自动循环渲染
        RENDER_CONTINUOUSLY,
        // 由外部经过notifySwap通知渲染
        RENDER_WHEN_DIRTY
    }
//-------------------------------------
}
复制代码

新增部分已经标出来,也不复杂,主要是新增了设置Surface,区分了两种渲染模式,请你们看代码便可。

  • 改造解码器

还记得以前的文章中提到,音视频要正常播放,须要对音频和视频进行音视频同步吗?

而因为编码的时候,并不须要把视频画面和音频播放出来,因此能够把音视频同步去掉,加快编码速度。

修改也很简单,在 BaseDecoder 中新增一个变量 mSyncRender ,若是 mSyncRender == false ,就把音视频同步去掉。

这里,只列出修改的部分,完整代码请看 BaseDecoder

abstract class BaseDecoder(private val mFilePath: String): IDecoder {
    
    // 省略无关代码......
    
    // 是否须要音视频渲染同步
    private var mSyncRender = true
    
    
    final override fun run() {
        //省略无关代码...
        
        while (mIsRunning) {
            // ......
            
            // ---------【音视频同步】-------------
            if (mSyncRender && mState == DecodeState.DECODING) {
                sleepRender()
            }
            
            if (mSyncRender) {// 若是只是用于编码合成新视频,无需渲染
                render(mOutputBuffers!![index], mBufferInfo)
            }
            
            // ......
        }
        //
    }
    
    override fun withoutSync(): IDecoder {
        mSyncRender = false
        return this
    }
    
    //......
}
复制代码
  • 整合
class SynthesizerActivity: AppCompatActivity(), MMuxer.IMuxerStateListener {

    private val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_2.mp4"
    private val path2 = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"

    private val threadPool = Executors.newFixedThreadPool(10)

    private var renderer = CustomerGLRenderer()

    private var audioDecoder: IDecoder? = null
    private var videoDecoder: IDecoder? = null

    private lateinit var videoEncoder: VideoEncoder
    private lateinit var audioEncoder: AudioEncoder

    private var muxer = MMuxer()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_synthesizer)
        muxer.setStateListener(this)
    }

    fun onStartClick(view: View) {
        btn.text = "正在编码"
        btn.isEnabled = false
        initVideo()
        initAudio()
        initAudioEncoder()
        initVideoEncoder()
    }

    private fun initVideoEncoder() {
        // 视频编码器
        videoEncoder = VideoEncoder(muxer, 1920, 1080)

        renderer.setRenderMode(CustomerGLRenderer.RenderMode.RENDER_WHEN_DIRTY)
        renderer.setSurface(videoEncoder.getEncodeSurface()!!, 1920, 1080)

        videoEncoder.setStateListener(object : DefEncodeStateListener {
            override fun encoderFinish(encoder: BaseEncoder) {
                renderer.stop()
            }
        })
        threadPool.execute(videoEncoder)
    }

    private fun initAudioEncoder() {
        // 音频编码器
        audioEncoder = AudioEncoder(muxer)
        // 启动编码线程
        threadPool.execute(audioEncoder)
    }

    private fun initVideo() {
        val drawer = VideoDrawer()
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initVideoDecoder(path, Surface(it))
        }
        renderer.addDrawer(drawer)
    }

    private fun initVideoDecoder(path: String, sf: Surface) {
        videoDecoder?.stop()
        videoDecoder = VideoDecoder(path, null, sf).withoutSync()
        videoDecoder!!.setStateListener(object : DefDecodeStateListener {
            override fun decodeOneFrame(decodeJob: BaseDecoder?, frame: Frame) {
                renderer.notifySwap(frame.bufferInfo.presentationTimeUs)
                videoEncoder.encodeOneFrame(frame)
            }

            override fun decoderFinish(decodeJob: BaseDecoder?) {
                videoEncoder.endOfStream()
            }
        })
        videoDecoder!!.goOn()

        //启动解码线程
        threadPool.execute(videoDecoder!!)
    }

    private fun initAudio() {
        audioDecoder?.stop()
        audioDecoder = AudioDecoder(path).withoutSync()
        audioDecoder!!.setStateListener(object : DefDecodeStateListener {

            override fun decodeOneFrame(decodeJob: BaseDecoder?, frame: Frame) {
                audioEncoder.encodeOneFrame(frame)
            }

            override fun decoderFinish(decodeJob: BaseDecoder?) {
                audioEncoder.endOfStream()
            }
        })
        audioDecoder!!.goOn()

        //启动解码线程
        threadPool.execute(audioDecoder!!)
    }

    override fun onMuxerFinish() {
    
        runOnUiThread {
            btn.isEnabled = true
            btn.text = "编码完成"
        }

        audioDecoder?.stop()
        audioDecoder = null

        videoDecoder?.stop()
        videoDecoder = null
    }
}
复制代码

能够看到,过程很简单:初始化解码器,初始化EGL Render,初始化编码器,而后将解码获得的数据扔到编码器队列中,监听解码状态和编码状态,作相应的操做。

解码过程和使用EGL播放视频基本是同样的,只是渲染模式不一样而已。

在这个代码中,只是简单的将原视频解码,渲染到OpenGL,从新编码成新的mp4,也就是说输出的视频和原视频是如出一辙的。

  • 能够实现什么?

虽然上面只是一个普通的解码和编码的过程,可是却能够衍生出无限的想象。

好比:

  • 实现视频裁剪:给解码器设置一个开始和结束的时间便可。

  • 实现炫酷的视频画面编辑:好比将视频渲染器 VideoDrawer 换成以前写好的 SoulVideoDrawer 的话,将获得一个有 灵魂出窍 效果的视频;结合以前的画中画,能够实现视频的叠加。

  • 视频拼接:结合多个视频解码器,将多个视频链接起来,编码成新的视频。

  • 加水印:结合OpenGL渲染图片,加个水印超简单的。

......

只要有想象力,那都不是事!

5、结束语

啊~~~,嗨森,终于写完本系列的【OpenGL渲染视频画面篇】,到目前为止,若是你看过每一篇文章,而且动手码过代码,我相信你必定已经踏入了Android音视频开发的大门,能够去实现一些之前看起来很神秘的视频效果,而后保存成一个真正的可播放的视频。

这一系列文章每篇都很长,感谢每一个能阅读到这里的读者,我以为咱们都应该感谢一下本身,坚持真的很难。

最后无比感谢每一位给文章点赞、留言、提问、鼓励的人儿,是大家让冰冷的文字充满温情,是我坚持的动力。

我们,下一篇章,不见不散!

相关文章
相关标签/搜索