激情六月丁香婷婷|亚洲色图AV二区|丝袜AV日韩AV|久草视频在线分类|伊人九九精品视频|国产精品一级电影|久草视频在线99|在线看的av网址|伊人99精品无码|午夜无码视频在线

高校合作1:010-59833514 ?咨詢電話:400-810-1418 服務(wù)與監(jiān)督電話:400-810-1418轉(zhuǎn)接2

移動(dòng)端使用OpenGL轉(zhuǎn)場(chǎng)特效的音視頻合成應(yīng)用

發(fā)布時(shí)間:2023-12-02 23:21:40 瀏覽量:248次

前言

近年來(lái)短視頻的火爆,讓內(nèi)容創(chuàng)作類的APP獲得了巨大的流量。用戶通過(guò)這類工具編輯自己的短視頻,添加各式各樣的炫酷特效,從而呈現(xiàn)出更加豐富多彩的視頻內(nèi)容。本文將會(huì)介紹如何使用移動(dòng)端原生API,將圖片添加轉(zhuǎn)場(chǎng)特效并且最終合成為視頻的基本流程。

一、音視頻基礎(chǔ)知識(shí)

我們經(jīng)常會(huì)和視頻打交道,最常見(jiàn)的就是MP4格式的視頻。這樣的視頻其實(shí)一般是由音頻和視頻組成的音視頻容器。下面先會(huì)介紹音視頻相關(guān)概念,為音視頻技術(shù)的應(yīng)用作一個(gè)鋪墊,希望能對(duì)音視頻頻開(kāi)發(fā)者提供一些幫助。

1.1 視頻的基礎(chǔ)知識(shí)

1.1.1 視頻幀

視頻中的一個(gè)基本概念就是幀,幀用來(lái)表示一個(gè)畫(huà)面。視頻的連續(xù)畫(huà)面就是由一個(gè)個(gè)連續(xù)的視頻幀組成。

1.1.2 幀率

幀率,F(xiàn)PS,全稱Frames Per Second。指每秒傳輸?shù)膸瑪?shù),或者每秒顯示的幀數(shù)。一般來(lái)說(shuō),幀率影響畫(huà)面流暢度,且成正比:幀率越大,畫(huà)面越流暢;幀率越小,畫(huà)面越有跳動(dòng)感。一個(gè)較權(quán)威的說(shuō)法:當(dāng)視頻幀率不低于24FPS時(shí),人眼才會(huì)覺(jué)得視頻是連貫的,稱為“視覺(jué)暫留”現(xiàn)象。16FPS可以達(dá)到一定的滿意程度,但效果略差。因此,才有說(shuō)法:盡管幀率越高越流暢,但在很多實(shí)際應(yīng)用場(chǎng)景中24FPS就可以了(電影標(biāo)準(zhǔn)24FPS,電視標(biāo)準(zhǔn)PAL制25FPS)。

1.1.3 分辨率

分辨率,Resolution,也常被俗稱為圖像的尺寸或者圖像的大小。指一幀圖像包含的像素的多少,常見(jiàn)有1280x720(720P),1920X1080(1080P)等規(guī)格。分辨率影響圖像大小,且與之成正比:分辨率越高,圖像越大;反之,圖像越小。

1.1.4 碼率

碼率,BPS,全稱Bits Per Second。指每秒傳送的數(shù)據(jù)位數(shù),常見(jiàn)單位KBPS(千位每秒)和MBPS(兆位每秒)。碼率是更廣泛的(視頻)質(zhì)量指標(biāo):更高的分辨率,更高的幀率和更低的壓縮率,都會(huì)導(dǎo)致碼率增加。

1.1.5 色彩空間

通常說(shuō)的色彩空間有兩種:

RGB:RGB的顏色模式應(yīng)該是我們最熟悉的一種,在現(xiàn)在的電子設(shè)備中應(yīng)用廣泛。通過(guò)R、G、B三種基礎(chǔ)色,可以混合出所有的顏色。

YUV:YUV是一種亮度與色度分離的色彩格式,三個(gè)字母的意義分別為:

Y:亮度,就是灰度值。除了表示亮度信號(hào)外,還含有較多的綠色通道量。單純的Y分量可以顯示出完整的黑白圖像。

U:藍(lán)色通道與亮度的差值。

V:紅色通道與亮度的差值。

其中,U、V分量分別表示藍(lán)(blue)、紅(red)分量信號(hào),只含有色度信息,所以YUV也稱為YCbCr,其中,Cb、Cr的含義等同于U、V,C可以理解為component或者color。

RGB和YUV的換算

YUV與RGB相互轉(zhuǎn)換的公式如下(RGB取值范圍均為0-255):

Y = 0.299R + 0.587G + 0.114BU = -0.147R - 0.289G + 0.436BV = 0.615R - 0.515G - 0.100B R = Y + 1.14VG = Y - 0.39U - 0.58VB = Y + 2.03U

1.2 音頻的基礎(chǔ)知識(shí)

音頻數(shù)據(jù)的承載方式最常用的是脈沖編碼調(diào)制,即PCM。

1.2.1 采樣率和采樣位數(shù)

采樣率是將聲音進(jìn)行數(shù)字化的采樣頻率,采樣位數(shù)與記錄聲波振幅有關(guān),位數(shù)越高,記錄的就越準(zhǔn)確。

1.2.2 聲道數(shù)

聲道數(shù),是指支持能不同發(fā)聲(注意是不同聲音)的音響的個(gè)數(shù)。

1.2.3 碼率

碼率,是指一個(gè)數(shù)據(jù)流中每秒鐘能通過(guò)的信息量,單位bps(bit per second)。

碼率 = 采樣率 * 采樣位數(shù) * 聲道數(shù)

上面介紹的音視頻的數(shù)據(jù)還需要進(jìn)行壓縮編碼,因?yàn)橐粢曨l的數(shù)據(jù)量都非常大,按照原始數(shù)據(jù)保存會(huì)非常的耗費(fèi)空間,而且想要傳輸這樣龐大的數(shù)據(jù)也很不方便。其實(shí)音視頻的原始數(shù)據(jù)中包含大量的重復(fù)數(shù)據(jù),特別是視頻,一幀一幀的畫(huà)面中包含大量的相似的內(nèi)容。所以需要對(duì)音視頻數(shù)據(jù)進(jìn)行編碼,以便于減小占用的空間,提高傳輸?shù)男省?/span>

1.3 視頻編碼

通俗地理解,例如一個(gè)視頻中,前一秒畫(huà)面跟當(dāng)前的畫(huà)面內(nèi)容相似度很高,那么這兩秒的數(shù)據(jù)是不是可以不用全部保存,只保留一個(gè)完整的畫(huà)面,下一個(gè)畫(huà)面看有哪些地方有變化了記錄下來(lái),拿視頻去播放的時(shí)候就按這個(gè)完整的畫(huà)面和其他有變化的地方把其他畫(huà)面也恢復(fù)出來(lái)。記錄畫(huà)面不同然后保存下來(lái)這個(gè)過(guò)程就是數(shù)據(jù)編碼,根據(jù)不同的地方恢復(fù)畫(huà)面的過(guò)程就是數(shù)據(jù)解碼。

一般常見(jiàn)的視頻編碼格式有H26x系列和MPEG系列。
H26x(1/2/3/4/5)系列由ITU(International Telecommunication Union)國(guó)際電傳視訊聯(lián)盟主導(dǎo)。
MPEG1/2/3/4)系列由MPEG(Moving Picture Experts Group, ISO旗下的組織)主導(dǎo)。

H264是新一代的編碼標(biāo)準(zhǔn),以高壓縮高質(zhì)量和支持多種網(wǎng)絡(luò)的流媒體傳輸著稱。iOS 8.0及以上蘋(píng)果開(kāi)放了VideoToolbox框架來(lái)實(shí)現(xiàn)H264硬編碼,開(kāi)發(fā)者可以利用VideoToolbox框架很方便地實(shí)現(xiàn)視頻的硬編碼。

H264編碼的優(yōu)勢(shì):

  • 低碼率
  • 高質(zhì)量的圖像
  • 容錯(cuò)能力強(qiáng)
  • 網(wǎng)絡(luò)適應(yīng)性強(qiáng)

H264最大的優(yōu)勢(shì),具有很高的數(shù)據(jù)壓縮比率,在同等圖像質(zhì)量下,H264的壓縮比是MPEG-2的2倍以上,MPEG-4的1.5~2倍。舉例: 原始文件的大小如果為88GB,采用MPEG-2壓縮標(biāo)準(zhǔn)壓縮后變成3.5GB,壓縮比為25∶1,而采用H.264壓縮標(biāo)準(zhǔn)壓縮后變?yōu)?79MB,從88GB到879MB,H.264的壓縮比達(dá)到驚人的102∶1。

1.4 音頻編碼

和視頻編碼一樣,音頻也有許多的編碼格式,如:WAV、MP3、WMA、APE、FLAC等等。

AAC

  • AAC是目前比較熱門的有損壓縮編碼技術(shù),并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三種主要編碼格式
  • 特點(diǎn):在小于128Kbit/s的碼率下表現(xiàn)優(yōu)異,并且多用于視頻中的音頻編碼
  • 使用場(chǎng)合:128Kbit/s以下的音頻編碼,多用于視頻中音頻軌的編碼

WAV

  • 在PCM數(shù)據(jù)格式的前面加上44字節(jié),描述PCM的采樣率、聲道數(shù)、數(shù)據(jù)格式等信息,不會(huì)壓縮
  • 特點(diǎn):音質(zhì)好,大量軟件支持
  • 使用場(chǎng)合:多媒體開(kāi)發(fā)的中間文件、保存音樂(lè)和音效素材

MP3

  • 使用LAME編碼
  • 特點(diǎn):音質(zhì)在128kbit/s以上表現(xiàn)不錯(cuò),壓縮比較高,大量軟件硬件都支持,兼容性好
  • 使用場(chǎng)合:高比特率(傳輸效率 bps, 這里的b是位,不是比特)對(duì)兼容性有要求的音樂(lè)欣賞

OGG

  • 特點(diǎn):可以用比MP3更小的碼率實(shí)現(xiàn)比MP3更好的音質(zhì),高中低碼率下均有良好的表現(xiàn)
  • 不足:兼容性不夠好,流媒體特性不支持
  • 適合場(chǎng)景:語(yǔ)音聊天的音頻消息場(chǎng)景

APE

  • 無(wú)損壓縮

FLAC

  • 專門針對(duì)PCM音頻的特點(diǎn)設(shè)計(jì)的壓縮方式,而且可以使用播放器直接播放FLAC壓縮的文件
  • 免費(fèi),支持大多數(shù)操作系統(tǒng)

二、使用OpenGL的底層轉(zhuǎn)場(chǎng)特效和原生平臺(tái)硬編碼進(jìn)行圖片、音樂(lè)、轉(zhuǎn)場(chǎng)合成視頻需要哪些 API

2.1 Android端和使用流程及相關(guān)API介紹

如果想要給圖片添加轉(zhuǎn)場(chǎng)特效并且合成為視頻,需要使用OpenGL對(duì)圖片進(jìn)行渲染,搭配自定義的轉(zhuǎn)場(chǎng)著色器,先讓圖片"動(dòng)起來(lái)"。然后使用MediaCodec將畫(huà)面內(nèi)容進(jìn)行編碼,然后使用MediaMuxer將編碼后的內(nèi)容打包成一個(gè)音視頻容器文件。

2.1.1 Mediacodec

MediaCodec是從API16后引入的處理音視頻編解碼的類,它可以直接訪問(wèn)Android底層的多媒體編解碼器,通常與MediaExtractor,MediaSync, MediaMuxer,MediaCrypto,MediaDrm,Image,Surface,以及AudioTrack一起使用。

下面是官網(wǎng)提供的MediaCodec工作的流程圖:

我們可以看到左邊是input,右邊是output。這里要分兩種情況來(lái)討論:

1)利用MediaCodec進(jìn)行解碼的時(shí)候,輸入input是待解碼的buffer數(shù)據(jù),輸出output是解碼好的buffer數(shù)據(jù)。

2)利用MediaCodec進(jìn)行編碼的時(shí)候,輸入input是一個(gè)待編碼的數(shù)據(jù),輸出output是編碼好的buffer數(shù)據(jù)。

    val width = 720
    val height = 1280
    val bitrate = 5000
    val encodeType = "video/avc"
    //配置用于編碼的MediaCodec
    val mCodec = MediaCodec.createEncoderByType(encodeType)
    val outputFormat = MediaFormat.createVideoFormat(encodeType, width, height)
    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)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        outputFormat.setInteger(
            MediaFormat.KEY_BITRATE_MODE,
            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
        )
    }
    codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    //這一步很關(guān)鍵,這一步得到的surface后面將會(huì)用到
    val mSurface = codec.createInputSurface()
    mCodec.start()
    val mOutputBuffers = mCodec.outputBuffers
    val mInputBuffers = mCodec.inputBuffers

以上是MediaCodec的作為編碼器的基本配置,其中
MediaCodec.createInputSurface()這個(gè)方法可以為我們創(chuàng)建一個(gè)用于向MediaCodec進(jìn)行輸入的surface。這樣通過(guò)MediaCodec就能獲取到編碼后的數(shù)據(jù)了。用這樣的方式編碼我們不需要向MedaiCodec輸入待編碼的數(shù)據(jù),MediaCodec會(huì)自動(dòng)將輸入到surface的數(shù)據(jù)進(jìn)行編碼。

2.1.2 EGL環(huán)境

OpenGL是一組用來(lái)操作GPU的API,但它并不能將繪制的內(nèi)容渲染到設(shè)備的窗口上,這里需要一個(gè)中間層,用來(lái)作為OpenGL和設(shè)備窗口之間的橋梁,并且最好是跨平臺(tái)的,這就是EGL,是由Khronos Group提供的一組平臺(tái)無(wú)關(guān)的API。

OpenGL繪制的內(nèi)容一般都是呈現(xiàn)在GLSurfaceView中的(GLSurfaceView的surface),如果我們需要將內(nèi)容編碼成視頻,需要將繪制的內(nèi)容渲染到MediaCodec提供的Surface中,然后獲取MediaCodec輸出的編碼后的數(shù)據(jù),封裝到指定的音視頻文件中。

創(chuàng)建EGL環(huán)境的主要步驟如下:

//1,創(chuàng)建 EGLDisplay
val mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
// 2,初始化 EGLDisplay
val version = IntArray(2)
EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)
// 3,初始化EGLConfig,EGLContext上下文
val config :EGLConfig? = null
if (mEGLContext === EGL14.EGL_NO_CONTEXT) {
    var renderableType = EGL14.EGL_OPENGL_ES2_BIT
        val attrList = intArrayOf(
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_RENDERABLE_TYPE, renderableType,
            EGL14.EGL_NONE, 0,
            EGL14.EGL_NONE
        )
        //配置Android指定的標(biāo)記
        if (flags and FLAG_RECORDABLE != 0) {
            attrList[attrList.size - 3] = EGL_RECORDABLE_ANDROID
            attrList[attrList.size - 2] = 1
        }
        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = IntArray(1)


        //獲取可用的EGL配置列表
        if (!EGL14.eglChooseConfig(mEGLDisplay, attrList, 0,
                configs, 0, configs.size,
                numConfigs, 0)) {
            configs[0]
        }
    val attr2List = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
    val context = EGL14.eglCreateContext(
        mEGLDisplay, config, sharedContext,
        attr2List, 0
    )
    mEGLConfig = config
    mEGLContext = context
}
//這里還需要?jiǎng)?chuàng)建一個(gè)EGL用于輸出的surface,這里的參數(shù)就可以傳入上一小節(jié)介紹的利用MeddiaCodec創(chuàng)建的Surface
 fun createWindowSurface(surface: Any): EGLSurface {
        val surfaceAttr = intArrayOf(EGL14.EGL_NONE)


        val eglSurface = EGL14.eglCreateWindowSurface(
                                        mEGLDisplay, mEGLConfig, surface,
                                        surfaceAttr, 0)


        if (eglSurface == null) {
            throw RuntimeException("Surface was null")
        }


        return eglSurface
    }

配置EGL環(huán)境后,還要一個(gè)surface作為輸出,這里就是要利用MediaCodec創(chuàng)建的surface作為輸出,即EGL的輸出作為MediaCodec的輸入。

2.1.3 MediaMuxer

MediaMuxer是Android平臺(tái)的音視頻合成工具,上面我們介紹了MediaCodec可以編碼數(shù)據(jù),EGL環(huán)境可以讓OpenGL程序?qū)⒗L制的內(nèi)容渲染到MediaCodec中,MediaCodec將這些數(shù)據(jù)編碼,最后這些編碼后的數(shù)據(jù)需要使用MediaMuxer寫(xiě)入到指定的文件中。

MediaMuxer基本使用:

//創(chuàng)建一個(gè)MediaMuxer,需要指定輸出保存的路徑,和輸出保存的格式。
val mediaMuxer = MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
//根據(jù)MediaFormat添加媒體軌道
mediaMuxer.addTrack(MediaFormat(...))
//將輸入的數(shù)據(jù),根據(jù)指定的軌道保存到指定的文件路徑中。
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
//結(jié)合上面的所說(shuō)的使用MediaCodec獲取到已編碼的數(shù)據(jù)
//當(dāng)前幀的信息
var mBufferInfo = MediaCodec.BufferInfo()
//編碼輸出緩沖區(qū)
var mOutputBuffers: Array<ByteBuffer>? = null
//獲取到編碼輸出的索引,寫(xiě)到指定的保存路徑
val index = mCodec.dequeueOutputBuffer(mBufferInfo, 1000)
muxer.writeSampleData(currentTrackIndex,mOutputBuffers[index],mBufferInfo)

2.1.4 MediaExtractor

MediaExtractor是Android平臺(tái)的多媒體提取器,能夠根據(jù)視頻軌道或者音頻軌道去提取對(duì)應(yīng)的數(shù)據(jù)。在進(jìn)行視頻編輯時(shí),可以利用MediaExtractor來(lái)提取指定的音頻信息,封裝到目標(biāo)音視頻文件中。

 //根據(jù)指定文件路徑創(chuàng)建MediaExtractor
 val mediaExtractor = MediaExtractor(...)
 //為MediaExtractor選擇好對(duì)應(yīng)的媒體軌道
 mediaExtractor.selectTrack(...)
 //讀取一幀的數(shù)據(jù)
 val inputBuffer = ByteBuffer.allocate(...)
 mediaExtractor.readSampleData(inputBuffer, 0)
 //進(jìn)入下一幀
 mediaExtractor.advance()
 //MediaExtractor讀取到的音頻數(shù)據(jù)可以使用MediaMuxer的writeSampleData方法寫(xiě)入到指定的文件中

以上就是利用Android平臺(tái)的硬編碼相關(guān)API,將OpenGL渲染到畫(huà)面編碼成視頻的基本流程介紹。

三、iOS端合成流程及相關(guān)API使用

由于AVFoundation原生框架對(duì)于圖層特效處理能力有限,無(wú)法直接生成和寫(xiě)入多張圖片之間切換的轉(zhuǎn)場(chǎng)效果,所以需要自行對(duì)圖片和音樂(lè)按照時(shí)間線,去實(shí)現(xiàn)音視頻數(shù)據(jù)從解碼到轉(zhuǎn)場(chǎng)特效應(yīng)用,以及最終寫(xiě)入文件的整個(gè)流程。

那么在多張圖片合成視頻的過(guò)程中,核心的部分就是如何處理多張圖片之間的轉(zhuǎn)場(chǎng)效果。這個(gè)時(shí)候我們需要配合OpenGL底層的特效能力,自定義濾鏡將即將要切換的2張圖片通過(guò)片元著色器生成新的紋理。本質(zhì)就是在這兩個(gè)紋理對(duì)象上去實(shí)現(xiàn)紋理和紋理之間的切換,通過(guò)Mix函數(shù)混合兩個(gè)紋理圖像,使用time在[0,1]之間不停變化來(lái)控制第二個(gè)圖片紋理混合的強(qiáng)弱變化從而實(shí)現(xiàn)漸變效果。接下來(lái)開(kāi)始介紹合成的流程和具體API的使用。

3.1 音視頻基礎(chǔ)API

在合成的過(guò)程中,我們使用到了AVAssetWriter這個(gè)類。AVAssetWriter可以將多媒體數(shù)據(jù)從多個(gè)源進(jìn)行編碼(比如接下來(lái)的多張圖片和一個(gè)BGM進(jìn)行合成)并寫(xiě)入指定文件格式的容器中,比如我們熟知的MPEG-4文件。

3.1.1 AVAssetWriter 與AVAssetWriterInput

AVAssetWriter通常由一個(gè)或多個(gè)AVAssetWriterInput對(duì)象構(gòu)成,將AVAssetWriterInput配置為可以處理指定的多媒體類型,比如音頻或視頻,用于添加將包含要寫(xiě)入容器的多媒體數(shù)據(jù)的CMSampleBufferRef對(duì)象。同時(shí)因?yàn)閍sset writer可以從多個(gè)數(shù)據(jù)源寫(xiě)入容器,因此必須要為寫(xiě)入文件的每個(gè)track(即音頻軌道、視頻軌道)創(chuàng)建一個(gè)對(duì)應(yīng)的AVAssetWriterInput對(duì)象。

AVAssetWriterInput可以設(shè)置視頻的主要參數(shù)如輸出碼率,幀率,最大幀間隔,編碼方式,輸出分辨率以及填充模式等。也可以設(shè)置音頻的主要參數(shù)如采樣率,聲道,編碼方式,輸出碼率等。

3.1.2 CMSampleBufferRef 與
AVAssetWriterInputPixelBufferAdaptor

CMSampleBuffer是一個(gè)基礎(chǔ)類,用于處理音視頻管道傳輸中的通用數(shù)據(jù)。CMSampleBuffer中包含零個(gè)或多個(gè)某一類型如音頻或者視頻的采樣數(shù)據(jù)??梢苑庋b音頻采集后、編碼后、解碼后的數(shù)據(jù)(PCM數(shù)據(jù)、AAC數(shù)據(jù))以及視頻編碼后的數(shù)據(jù)(H.264數(shù)據(jù))。而CMSampleBufferRef是對(duì)CMSampleBuffer的一種引用。在提取音頻的時(shí)候,像如下的使用方式同步復(fù)制輸出的下一個(gè)示例緩沖區(qū)。

CMSampleBufferRef sampleBuffer = [assetReaderAudioOutput copyNextSampleBuffer];

每個(gè)AVAssetWriterInput期望以CMSampleBufferRef對(duì)象形式接收數(shù)據(jù),如果在處理視頻樣本的數(shù)據(jù)時(shí),便要將CVPixelBufferRef類型對(duì)象(像素緩沖樣本數(shù)據(jù))添加到asset writer input,這個(gè)時(shí)候就需要使用
AVAssetWriterInputPixelBufferAdaptor 這個(gè)專門的適配器類。這個(gè)類在附加被包裝為CVPixelBufferRef對(duì)象的視頻樣本時(shí)提供最佳性能。


AVAssetWriterInputPixelBufferAdaptor它是一個(gè)輸入的像素緩沖適配器,作為assetWriter的視頻輸入源,用于把緩沖池中的像素打包追加到視頻樣本上。在寫(xiě)入文件的時(shí)候,需要將CMSampleBufferRef轉(zhuǎn)成CVPixelBuffer,而這個(gè)轉(zhuǎn)換是在CVPixelBufferPool中完成的。
AVAssetWriterInputPixelBufferAdaptor的實(shí)例提供了一個(gè)CVPixelBufferPool,可用于分配像素緩沖區(qū)來(lái)寫(xiě)入輸出數(shù)據(jù)。使用它提供的像素緩沖池進(jìn)行緩沖區(qū)分配通常比使用額外創(chuàng)建的緩沖區(qū)更加高效。

CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferPoolCreatePixelBuffer(NULL, self.inputPixelBufferAdptor.pixelBufferPool,&pixelBuffer);

每個(gè)
AVAssetWriterInputPixelBufferAdaptor都包含一個(gè)assetWriterInput,用于接收緩沖區(qū)中的數(shù)據(jù),并且AVAssetWriterInput有一個(gè)很重要的屬性readyForMoreMediaData,來(lái)標(biāo)識(shí)現(xiàn)在緩沖區(qū)中的數(shù)據(jù)是否已經(jīng)處理完成。通過(guò)判斷這個(gè)屬性,我們可以向
AVAssetWriterInputPixelBufferAdaptor中添加數(shù)據(jù)(appendPixelBuffer:)以進(jìn)行處理。

if(self.inputPixelBufferAdptor.assetWriterInput.isReadyForMoreMediaData) {
    BOOL success = [self.inputPixelBufferAdptor appendPixelBuffer:newPixelBuffer withPresentationTime:self.currentSampleTime];    


    if (success) {
        NSLog(@"append buffer success");
    }
}

3.1.3 設(shè)置輸入輸出參數(shù),以及多媒體數(shù)據(jù)的采樣

第一步:創(chuàng)建AVAssetWriter對(duì)象,傳入生成視頻的路徑和格式

AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outFilePath] fileType:AVFileTypeMPEG4 error:&outError];

第二步:設(shè)置輸出視頻相關(guān)信息,如大小,編碼格式H264,以及創(chuàng)建視頻的輸入類videoWriterInput,以便后續(xù)給assetReader添加videoWriterInput。

CGSize size = CGSizeMake(480, 960);


NSDictionary *videoSetDic = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecTypeH264,AVVideoCodecKey,
[NSNumber numberWithInt:size.width],AVVideoWidthKey,[NSNumber numberWithInt:size.height],AVVideoHeightKey,nil];


AVAssetWriterInput *videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSetDic];


//將讀取的圖片內(nèi)容添加到assetWriter                                              
if ([assetWriter canAddInput:videoWriterInput]) {
    [assetWriter addInput:videoWriterInput];
}

第三步:創(chuàng)建一個(gè)處理視頻樣本時(shí)專用的適配器對(duì)象,這個(gè)類在附加被包裝為CVPixelBufferRef對(duì)象的視頻樣本時(shí)能提供最優(yōu)性能。如果想要將CVPixelBufferRef類型對(duì)象添加到asset writer input,就需要使用
AVAssetWriterInputPixelBufferAdaptor類。

NSDictionary *pixelBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],kCVPixelBufferPixelFormatTypeKey,nil];


AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput pixelBufferAttributes:pixelBufferAttributes];

第四步:音頻數(shù)據(jù)的采集、添加音頻輸入

//創(chuàng)建音頻資源
AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];
//創(chuàng)建音頻Track
AVAssetTrack *assetAudioTrack = [audioAsset tracksWithMediaType:AVMediaTypeAudio].firstObject;
//創(chuàng)建讀取器 
AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:audioAsset error:&error];
//讀取音頻track中的數(shù)據(jù)
NSDictionary *audioSettings = @{AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]};


AVAssetReaderTrackOutput *assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings: audioSettings];
//向接收器添加assetReaderAudioOutput輸出
if ([assetReader canAddOutput:assetReaderAudioOutput]) {
    [assetReader addOutput:assetReaderAudioOutput];
}


//音頻通道數(shù)據(jù),設(shè)置音頻的比特率、采樣率的通道數(shù)
AudioChannelLayout acl;
bzero( &acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;


NSData *channelLayoutAsData = [NSData dataWithBytes:&acl length:offsetof(AudioChannelLayout, acl)];


NSDictionary *audioSettings = @{AVFormatIDKey:[NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC],AVEncoderBitRateKey:[NSNumber numberWithInteger:128000], AVSampleRateKey:[NSNumber numberWithInteger:44100], AVChannelLayoutKey:channelLayoutAsData,AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2]};
//創(chuàng)建音頻的assetWriterAudioInput,將讀取的音頻內(nèi)容添加到assetWriter
AVAssetWriterInput *assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings: audioSettings];


if ([assetWriter canAddInput:assetWriterAudioInput]) {
    [assetWriter addInput:assetWriterAudioInput];
}


//Writer開(kāi)始進(jìn)行寫(xiě)入流程
[assetWriter startSessionAtSourceTime:kCMTimeZero];

3.2 轉(zhuǎn)場(chǎng)切換效果中的圖片處理

上面介紹了音視頻合成的大致流程,但是核心的部分是在于我們?cè)诤铣梢曨l時(shí),如何去寫(xiě)入第一張和第二張圖片展示間隙中的切換過(guò)程效果。這個(gè)時(shí)候就得引入GPUImage這個(gè)底層框架,而GPUImage是iOS端對(duì)OpenGL的封裝。即我們通過(guò)繼承GPUImageFilter去實(shí)現(xiàn)自定義濾鏡,并重寫(xiě)片元著色器的效果,通過(guò)如下代理回調(diào)得到這個(gè)過(guò)程中返回的一系列處理好的紋理樣本數(shù)據(jù)。

-(void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;

然后轉(zhuǎn)換成相應(yīng)的pixelBuffer數(shù)據(jù),通過(guò)調(diào)用appendPixelBuffer:添加到幀緩存中去,從而寫(xiě)入到文件中。

-(BOOL)appendPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;

3.2.1 如何自定義濾鏡

在GPUImageFilter中默認(rèn)的著色器程序比較簡(jiǎn)單,只是簡(jiǎn)單的進(jìn)行紋理采樣,并沒(méi)有對(duì)像素?cái)?shù)據(jù)進(jìn)行相關(guān)操作。所以在自定義相關(guān)濾鏡的時(shí)候,我們通常需要自定義片段著色器的效果來(lái)處理紋理效果從而達(dá)到豐富的轉(zhuǎn)場(chǎng)效果。

我們通過(guò)繼承GPUImageFilter來(lái)自定義我們轉(zhuǎn)場(chǎng)效果所需的濾鏡,首先是創(chuàng)建一個(gè)濾鏡文件compositeImageFilter繼承于GPUImageFilter,然后重寫(xiě)父類的方法去初始化頂點(diǎn)和片段著色器。

- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;

這個(gè)時(shí)候需要傳入所需的片元著色器代碼,那么怎么自定義GLSL文件呢,以下便是如何編寫(xiě)具體的GLSL文件,即片元著色器實(shí)現(xiàn)代碼。傳入紋理的頂點(diǎn)坐標(biāo)textureCoordinate、2張圖片的紋理imageTexture、imageTexture2,通過(guò)mix函數(shù)混合兩個(gè)紋理圖像,使用time在[0,1]之間不停變化來(lái)控制第二個(gè)圖片紋理混合的強(qiáng)弱變化從而實(shí)現(xiàn)漸變效果。

precision highp float;
varying highp vec2 textureCoordinate;
uniform sampler2D imageTexture;
uniform sampler2D imageTexture2;
uniform mediump vec4 v4Param1;
float progress = v4Param1.x;
void main()
{
    vec4 color1 = texture2D(imageTexture, textureCoordinate);
    vec4 color2 = texture2D(imageTextur2, textureCoordinate);
    gl_FragColor = mix(color1, color2, step(1.0-textureCoordinate.x,progress));
}

3.2.2 了解GPUImageFilter中重點(diǎn)API

在GPUImageFilter中有三個(gè)最重要的API,GPUImageFilter會(huì)將接收到的幀緩存對(duì)象經(jīng)過(guò)特定的片段著色器繪制到即將輸出的幀緩存對(duì)象中,然后將自己輸出的幀緩存對(duì)象傳給所有Targets并通知它們進(jìn)行處理。方法被調(diào)用的順序:

1)生成新的幀緩存對(duì)象

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;

2)進(jìn)行紋理的繪制

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;

3)繪制完成通知所有的target處理下一幀的紋理數(shù)據(jù)

- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;

通過(guò)如上代理回調(diào)就可以得到這個(gè)過(guò)程中返回的一系列處理好的紋理樣本數(shù)據(jù)。

按照方法調(diào)用順序,我們一般先重寫(xiě)newFrameReadyAtTime方法,構(gòu)建最新的頂點(diǎn)坐標(biāo),生成新的幀緩存對(duì)象。

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    static const GLfloat imageVertices[] = {
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f,  1.0f,
        1.0f,  1.0f,
    };
    [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];
}

然后在這個(gè)方法中調(diào)用
renderToTextureWithVertices去繪制所需的紋理,并獲取到最終的幀緩存對(duì)象。以下是部分核心代碼:

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates {
glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
glUniform1i(filterInputTextureUniform, 5);
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, [self adjustVertices:vertices]);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glFinish();
CVPixelBufferRef pixel_buffer = NULL;
CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, [self.videoPixelBufferAdaptor pixelBufferPool], &pixel_buffer);
if ((pixel_buffer == NULL) || (status != kCVReturnSuccess)) {
    CVPixelBufferRelease(pixel_buffer);
    return;
} else {
    CVPixelBufferLockBaseAddress(pixel_buffer, 0);
    GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
    glReadPixels(0, 0, self.sizeOfFBO.width, self.sizeOfFBO.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData);
CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
        }
    }
}

3.2.3 pixel_buffer的寫(xiě)入

在上述的處理過(guò)程當(dāng)中,我們便獲取到了所需的幀緩存樣本數(shù)據(jù)pixel_buffer。而這個(gè)數(shù)據(jù)便是合成轉(zhuǎn)場(chǎng)切換過(guò)程中的數(shù)據(jù),我們把它進(jìn)行寫(xiě)入,自此便完成了第一張和第二張圖片轉(zhuǎn)場(chǎng)效果效果的寫(xiě)入。待轉(zhuǎn)場(chǎng)效果寫(xiě)入之后,我們便可按照此流程根據(jù)時(shí)間的進(jìn)度寫(xiě)入第二張圖片以及后續(xù)的第二張圖片和第三張圖片的轉(zhuǎn)場(chǎng)效果。依此類推,一直到寫(xiě)完所有的圖片。

CVPixelBufferLockBaseAddress(pixelBuffer, 0);
if (self.assetWriter.status != AVAssetWriterStatusWriting) {
    [self.assetWriter startWriting];
}
[self.assetWriter startSessionAtSourceTime:frameTime];
if (self.assetWriter.status == AVAssetWriterStatusWriting) {
    if (CMTIME_IS_NUMERIC(frameTime) == NO)  {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        return;
    }
    //確定寫(xiě)操作是否已完成、失敗或已取消
    if ([self.videoPixelBufferAdaptor appendPixelBuffer:pixelBufferwithPresentationTime:frameTime]) {                                                    
        NSLog(@"%f", CMTimeGetSeconds(frameTime));
    }
}else {
    NSLog(@"status:%d", self.assetWriter.status);
    }
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);

以上便是在iOS端處理音視頻合成的具體步驟,難點(diǎn)在于如何使用GPUImage去實(shí)現(xiàn)復(fù)雜的轉(zhuǎn)場(chǎng)效果并將其寫(xiě)到到容器中。

本文介紹了音視頻相關(guān)的基本知識(shí),讓大家對(duì)音視頻的關(guān)鍵概念有了一些理解。然后分別介紹了Android和iOS這兩個(gè)移動(dòng)平臺(tái)音視頻編解碼API,利用這些平臺(tái)自帶的API,我們可以將OpenGL渲染的畫(huà)面編碼成音視頻文件。鑒于篇幅限制,文中的流程只截取了部分關(guān)鍵步驟的代碼,歡迎大家來(lái)交流音視頻相關(guān)的知識(shí)。

作者簡(jiǎn)介

jzg,攜程資深前端開(kāi)發(fā)工程師,專注Android開(kāi)發(fā);

zx,攜程高級(jí)前端開(kāi)發(fā)工程師,專注iOS開(kāi)發(fā);

zcc,攜程資深前端開(kāi)發(fā)工程師,專注iOS開(kāi)發(fā)。

熱門課程推薦

熱門資訊

請(qǐng)綁定手機(jī)號(hào)

x

同學(xué)您好!

您已成功報(bào)名0元試學(xué)活動(dòng),老師會(huì)在第一時(shí)間與您取得聯(lián)系,請(qǐng)保持電話暢通!
確定