發(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)特效并且最終合成為視頻的基本流程。
我們經(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)。
MPEG(1/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ì):
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
WAV
MP3
OGG
APE
FLAC
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à)面編碼成視頻的基本流程介紹。
由于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ā)。
熱門資訊
1. 動(dòng)畫(huà)制作VS影視特效!到底有什么不同?
想了解動(dòng)畫(huà)制作和影視特效的區(qū)別嗎?本文將帶您深入探討動(dòng)畫(huà)制作和影視特效之間的關(guān)系,幫助你更好地理解這兩者的差異。
2. 快影、剪映、快剪輯三款軟件對(duì)比評(píng)測(cè),哪款更適合小白?
想知道快影、剪映、快剪輯這三款軟件哪個(gè)更適合小白?看看這篇對(duì)比評(píng)測(cè),帶你了解這三款軟件的功能和特點(diǎn),快速選擇適合自己的視頻剪輯軟件。
3. 剪映專業(yè)版時(shí)間軌道軌道調(diào)整技巧
剪映專業(yè)版新增全局預(yù)覽縮放功能,可以輕松放大或縮小時(shí)間軌道。學(xué)習(xí)如何使用時(shí)間線縮放功能,提升剪輯效率。
4. 豆瓣8.3《鐵皮鼓》|電影符號(hào)學(xué)背后的視覺(jué)盛宴、社會(huì)隱喻主題
文|悅兒(叮咚,好電影來(lái)了!)《鐵皮鼓》是施隆多夫最具代表性的作品,影片于... 分析影片的社會(huì)隱喻主題;以及對(duì)于普通觀眾來(lái)說(shuō),它又帶給我們哪些現(xiàn)實(shí)啟發(fā)...
5. 從宏觀蒙太奇思維、中觀敘事結(jié)構(gòu)、微觀剪輯手法解讀《花樣年華》
中觀層面完成敘事結(jié)構(gòu)、以及微觀層面的剪輯手法,3個(gè)層次來(lái)解讀下電影《花樣年華》的蒙太奇魅力。一、 宏觀層面:運(yùn)用蒙太奇思維構(gòu)建電影劇本雛形。蒙...
6. 零基礎(chǔ)怎么學(xué)習(xí)視頻剪輯?新手視頻剪輯教程
1、每個(gè)切點(diǎn)需要理由和動(dòng)機(jī) 很剪輯師認(rèn)為,賦予每一個(gè)切點(diǎn)動(dòng)機(jī)是非常困難的。很多...
7. 15種電影剪輯/轉(zhuǎn)場(chǎng)藝術(shù),賦予影片絕妙魅力
15種電影剪輯/轉(zhuǎn)場(chǎng)手法,讓影片更吸引眼球!回顧電影中豐富多樣的專場(chǎng)技巧,比如瞬間從一個(gè)場(chǎng)景中變換到空中... 現(xiàn)在是測(cè)試技術(shù)的時(shí)候了!以下是一些常見(jiàn)剪輯手法,讓你觀影過(guò)程更加華麗動(dòng)人!
本文介紹了十款強(qiáng)大的PR視頻剪輯插件,幫助提升視頻剪輯效率,提高創(chuàng)作品質(zhì),并降低創(chuàng)作難度。
9. 《肖申克的救贖》通過(guò)鏡頭語(yǔ)言,向觀眾展現(xiàn)了安迪自我救贖的過(guò)程
以突出劇情的緊張氛圍和角色的情感變化。此外,電影的拍攝手法和剪輯方案還與影片的敘事結(jié)構(gòu)和主題緊密相連。導(dǎo)演巧妙地運(yùn)用回憶、閃回和象征性鏡頭等...
10. 淺析電影的三種隱喻形式——白日夢(mèng)、鏡子、窺視窗
電影創(chuàng)作者可以通過(guò)表意、造型、畫(huà)面展示等元素對(duì)隱身性的含義進(jìn)行隱喻,打... 電影和夢(mèng)境都具有普遍性的象征意義,夢(mèng)境中的元素能夠代表人內(nèi)心的欲望,...
最新文章
同學(xué)您好!