發(fā)布時間:2023-12-02 23:21:40 瀏覽量:250次
近年來短視頻的火爆,讓內(nèi)容創(chuàng)作類的APP獲得了巨大的流量。用戶通過這類工具編輯自己的短視頻,添加各式各樣的炫酷特效,從而呈現(xiàn)出更加豐富多彩的視頻內(nèi)容。本文將會介紹如何使用移動端原生API,將圖片添加轉(zhuǎn)場特效并且最終合成為視頻的基本流程。
我們經(jīng)常會和視頻打交道,最常見的就是MP4格式的視頻。這樣的視頻其實一般是由音頻和視頻組成的音視頻容器。下面先會介紹音視頻相關(guān)概念,為音視頻技術(shù)的應用作一個鋪墊,希望能對音視頻頻開發(fā)者提供一些幫助。
1.1 視頻的基礎知識
1.1.1 視頻幀
視頻中的一個基本概念就是幀,幀用來表示一個畫面。視頻的連續(xù)畫面就是由一個個連續(xù)的視頻幀組成。
1.1.2 幀率
幀率,F(xiàn)PS,全稱Frames Per Second。指每秒傳輸?shù)膸瑪?shù),或者每秒顯示的幀數(shù)。一般來說,幀率影響畫面流暢度,且成正比:幀率越大,畫面越流暢;幀率越小,畫面越有跳動感。一個較權(quán)威的說法:當視頻幀率不低于24FPS時,人眼才會覺得視頻是連貫的,稱為“視覺暫留”現(xiàn)象。16FPS可以達到一定的滿意程度,但效果略差。因此,才有說法:盡管幀率越高越流暢,但在很多實際應用場景中24FPS就可以了(電影標準24FPS,電視標準PAL制25FPS)。
1.1.3 分辨率
分辨率,Resolution,也常被俗稱為圖像的尺寸或者圖像的大小。指一幀圖像包含的像素的多少,常見有1280x720(720P),1920X1080(1080P)等規(guī)格。分辨率影響圖像大小,且與之成正比:分辨率越高,圖像越大;反之,圖像越小。
1.1.4 碼率
碼率,BPS,全稱Bits Per Second。指每秒傳送的數(shù)據(jù)位數(shù),常見單位KBPS(千位每秒)和MBPS(兆位每秒)。碼率是更廣泛的(視頻)質(zhì)量指標:更高的分辨率,更高的幀率和更低的壓縮率,都會導致碼率增加。
1.1.5 色彩空間
通常說的色彩空間有兩種:
RGB:RGB的顏色模式應該是我們最熟悉的一種,在現(xiàn)在的電子設備中應用廣泛。通過R、G、B三種基礎色,可以混合出所有的顏色。
YUV:YUV是一種亮度與色度分離的色彩格式,三個字母的意義分別為:
Y:亮度,就是灰度值。除了表示亮度信號外,還含有較多的綠色通道量。單純的Y分量可以顯示出完整的黑白圖像。
U:藍色通道與亮度的差值。
V:紅色通道與亮度的差值。
其中,U、V分量分別表示藍(blue)、紅(red)分量信號,只含有色度信息,所以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 音頻的基礎知識
音頻數(shù)據(jù)的承載方式最常用的是脈沖編碼調(diào)制,即PCM。
1.2.1 采樣率和采樣位數(shù)
采樣率是將聲音進行數(shù)字化的采樣頻率,采樣位數(shù)與記錄聲波振幅有關(guān),位數(shù)越高,記錄的就越準確。
1.2.2 聲道數(shù)
聲道數(shù),是指支持能不同發(fā)聲(注意是不同聲音)的音響的個數(shù)。
1.2.3 碼率
碼率,是指一個數(shù)據(jù)流中每秒鐘能通過的信息量,單位bps(bit per second)。
碼率 = 采樣率 * 采樣位數(shù) * 聲道數(shù)
上面介紹的音視頻的數(shù)據(jù)還需要進行壓縮編碼,因為音視頻的數(shù)據(jù)量都非常大,按照原始數(shù)據(jù)保存會非常的耗費空間,而且想要傳輸這樣龐大的數(shù)據(jù)也很不方便。其實音視頻的原始數(shù)據(jù)中包含大量的重復數(shù)據(jù),特別是視頻,一幀一幀的畫面中包含大量的相似的內(nèi)容。所以需要對音視頻數(shù)據(jù)進行編碼,以便于減小占用的空間,提高傳輸?shù)男省?/span>
1.3 視頻編碼
通俗地理解,例如一個視頻中,前一秒畫面跟當前的畫面內(nèi)容相似度很高,那么這兩秒的數(shù)據(jù)是不是可以不用全部保存,只保留一個完整的畫面,下一個畫面看有哪些地方有變化了記錄下來,拿視頻去播放的時候就按這個完整的畫面和其他有變化的地方把其他畫面也恢復出來。記錄畫面不同然后保存下來這個過程就是數(shù)據(jù)編碼,根據(jù)不同的地方恢復畫面的過程就是數(shù)據(jù)解碼。
一般常見的視頻編碼格式有H26x系列和MPEG系列。
H26x(1/2/3/4/5)系列由ITU(International Telecommunication Union)國際電傳視訊聯(lián)盟主導。
MPEG(1/2/3/4)系列由MPEG(Moving Picture Experts Group, ISO旗下的組織)主導。
H264是新一代的編碼標準,以高壓縮高質(zhì)量和支持多種網(wǎng)絡的流媒體傳輸著稱。iOS 8.0及以上蘋果開放了VideoToolbox框架來實現(xiàn)H264硬編碼,開發(fā)者可以利用VideoToolbox框架很方便地實現(xiàn)視頻的硬編碼。
H264編碼的優(yōu)勢:
H264最大的優(yōu)勢,具有很高的數(shù)據(jù)壓縮比率,在同等圖像質(zhì)量下,H264的壓縮比是MPEG-2的2倍以上,MPEG-4的1.5~2倍。舉例: 原始文件的大小如果為88GB,采用MPEG-2壓縮標準壓縮后變成3.5GB,壓縮比為25∶1,而采用H.264壓縮標準壓縮后變?yōu)?79MB,從88GB到879MB,H.264的壓縮比達到驚人的102∶1。
1.4 音頻編碼
和視頻編碼一樣,音頻也有許多的編碼格式,如:WAV、MP3、WMA、APE、FLAC等等。
AAC
WAV
MP3
OGG
APE
FLAC
2.1 Android端和使用流程及相關(guān)API介紹
如果想要給圖片添加轉(zhuǎn)場特效并且合成為視頻,需要使用OpenGL對圖片進行渲染,搭配自定義的轉(zhuǎn)場著色器,先讓圖片"動起來"。然后使用MediaCodec將畫面內(nèi)容進行編碼,然后使用MediaMuxer將編碼后的內(nèi)容打包成一個音視頻容器文件。
2.1.1 Mediacodec
MediaCodec是從API16后引入的處理音視頻編解碼的類,它可以直接訪問Android底層的多媒體編解碼器,通常與MediaExtractor,MediaSync, MediaMuxer,MediaCrypto,MediaDrm,Image,Surface,以及AudioTrack一起使用。
下面是官網(wǎng)提供的MediaCodec工作的流程圖:
我們可以看到左邊是input,右邊是output。這里要分兩種情況來討論:
1)利用MediaCodec進行解碼的時候,輸入input是待解碼的buffer數(shù)據(jù),輸出output是解碼好的buffer數(shù)據(jù)。
2)利用MediaCodec進行編碼的時候,輸入input是一個待編碼的數(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后面將會用到
val mSurface = codec.createInputSurface()
mCodec.start()
val mOutputBuffers = mCodec.outputBuffers
val mInputBuffers = mCodec.inputBuffers
以上是MediaCodec的作為編碼器的基本配置,其中
MediaCodec.createInputSurface()這個方法可以為我們創(chuàng)建一個用于向MediaCodec進行輸入的surface。這樣通過MediaCodec就能獲取到編碼后的數(shù)據(jù)了。用這樣的方式編碼我們不需要向MedaiCodec輸入待編碼的數(shù)據(jù),MediaCodec會自動將輸入到surface的數(shù)據(jù)進行編碼。
2.1.2 EGL環(huán)境
OpenGL是一組用來操作GPU的API,但它并不能將繪制的內(nèi)容渲染到設備的窗口上,這里需要一個中間層,用來作為OpenGL和設備窗口之間的橋梁,并且最好是跨平臺的,這就是EGL,是由Khronos Group提供的一組平臺無關(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指定的標記
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
}
//這里還需要創(chuàng)建一個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)境后,還要一個surface作為輸出,這里就是要利用MediaCodec創(chuàng)建的surface作為輸出,即EGL的輸出作為MediaCodec的輸入。
2.1.3 MediaMuxer
MediaMuxer是Android平臺的音視頻合成工具,上面我們介紹了MediaCodec可以編碼數(shù)據(jù),EGL環(huán)境可以讓OpenGL程序?qū)⒗L制的內(nèi)容渲染到MediaCodec中,MediaCodec將這些數(shù)據(jù)編碼,最后這些編碼后的數(shù)據(jù)需要使用MediaMuxer寫入到指定的文件中。
MediaMuxer基本使用:
//創(chuàng)建一個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é)合上面的所說的使用MediaCodec獲取到已編碼的數(shù)據(jù)
//當前幀的信息
var mBufferInfo = MediaCodec.BufferInfo()
//編碼輸出緩沖區(qū)
var mOutputBuffers: Array<ByteBuffer>? = null
//獲取到編碼輸出的索引,寫到指定的保存路徑
val index = mCodec.dequeueOutputBuffer(mBufferInfo, 1000)
muxer.writeSampleData(currentTrackIndex,mOutputBuffers[index],mBufferInfo)
2.1.4 MediaExtractor
MediaExtractor是Android平臺的多媒體提取器,能夠根據(jù)視頻軌道或者音頻軌道去提取對應的數(shù)據(jù)。在進行視頻編輯時,可以利用MediaExtractor來提取指定的音頻信息,封裝到目標音視頻文件中。
//根據(jù)指定文件路徑創(chuàng)建MediaExtractor
val mediaExtractor = MediaExtractor(...)
//為MediaExtractor選擇好對應的媒體軌道
mediaExtractor.selectTrack(...)
//讀取一幀的數(shù)據(jù)
val inputBuffer = ByteBuffer.allocate(...)
mediaExtractor.readSampleData(inputBuffer, 0)
//進入下一幀
mediaExtractor.advance()
//MediaExtractor讀取到的音頻數(shù)據(jù)可以使用MediaMuxer的writeSampleData方法寫入到指定的文件中
以上就是利用Android平臺的硬編碼相關(guān)API,將OpenGL渲染到畫面編碼成視頻的基本流程介紹。
由于AVFoundation原生框架對于圖層特效處理能力有限,無法直接生成和寫入多張圖片之間切換的轉(zhuǎn)場效果,所以需要自行對圖片和音樂按照時間線,去實現(xiàn)音視頻數(shù)據(jù)從解碼到轉(zhuǎn)場特效應用,以及最終寫入文件的整個流程。
那么在多張圖片合成視頻的過程中,核心的部分就是如何處理多張圖片之間的轉(zhuǎn)場效果。這個時候我們需要配合OpenGL底層的特效能力,自定義濾鏡將即將要切換的2張圖片通過片元著色器生成新的紋理。本質(zhì)就是在這兩個紋理對象上去實現(xiàn)紋理和紋理之間的切換,通過Mix函數(shù)混合兩個紋理圖像,使用time在[0,1]之間不停變化來控制第二個圖片紋理混合的強弱變化從而實現(xiàn)漸變效果。接下來開始介紹合成的流程和具體API的使用。
3.1 音視頻基礎API
在合成的過程中,我們使用到了AVAssetWriter這個類。AVAssetWriter可以將多媒體數(shù)據(jù)從多個源進行編碼(比如接下來的多張圖片和一個BGM進行合成)并寫入指定文件格式的容器中,比如我們熟知的MPEG-4文件。
3.1.1 AVAssetWriter 與AVAssetWriterInput
AVAssetWriter通常由一個或多個AVAssetWriterInput對象構(gòu)成,將AVAssetWriterInput配置為可以處理指定的多媒體類型,比如音頻或視頻,用于添加將包含要寫入容器的多媒體數(shù)據(jù)的CMSampleBufferRef對象。同時因為asset writer可以從多個數(shù)據(jù)源寫入容器,因此必須要為寫入文件的每個track(即音頻軌道、視頻軌道)創(chuàng)建一個對應的AVAssetWriterInput對象。
AVAssetWriterInput可以設置視頻的主要參數(shù)如輸出碼率,幀率,最大幀間隔,編碼方式,輸出分辨率以及填充模式等。也可以設置音頻的主要參數(shù)如采樣率,聲道,編碼方式,輸出碼率等。
3.1.2 CMSampleBufferRef 與
AVAssetWriterInputPixelBufferAdaptor
CMSampleBuffer是一個基礎類,用于處理音視頻管道傳輸中的通用數(shù)據(jù)。CMSampleBuffer中包含零個或多個某一類型如音頻或者視頻的采樣數(shù)據(jù)??梢苑庋b音頻采集后、編碼后、解碼后的數(shù)據(jù)(PCM數(shù)據(jù)、AAC數(shù)據(jù))以及視頻編碼后的數(shù)據(jù)(H.264數(shù)據(jù))。而CMSampleBufferRef是對CMSampleBuffer的一種引用。在提取音頻的時候,像如下的使用方式同步復制輸出的下一個示例緩沖區(qū)。
CMSampleBufferRef sampleBuffer = [assetReaderAudioOutput copyNextSampleBuffer];
每個AVAssetWriterInput期望以CMSampleBufferRef對象形式接收數(shù)據(jù),如果在處理視頻樣本的數(shù)據(jù)時,便要將CVPixelBufferRef類型對象(像素緩沖樣本數(shù)據(jù))添加到asset writer input,這個時候就需要使用
AVAssetWriterInputPixelBufferAdaptor 這個專門的適配器類。這個類在附加被包裝為CVPixelBufferRef對象的視頻樣本時提供最佳性能。
AVAssetWriterInputPixelBufferAdaptor它是一個輸入的像素緩沖適配器,作為assetWriter的視頻輸入源,用于把緩沖池中的像素打包追加到視頻樣本上。在寫入文件的時候,需要將CMSampleBufferRef轉(zhuǎn)成CVPixelBuffer,而這個轉(zhuǎn)換是在CVPixelBufferPool中完成的。
AVAssetWriterInputPixelBufferAdaptor的實例提供了一個CVPixelBufferPool,可用于分配像素緩沖區(qū)來寫入輸出數(shù)據(jù)。使用它提供的像素緩沖池進行緩沖區(qū)分配通常比使用額外創(chuàng)建的緩沖區(qū)更加高效。
CVPixelBufferRef pixelBuffer = NULL;
CVPixelBufferPoolCreatePixelBuffer(NULL, self.inputPixelBufferAdptor.pixelBufferPool,&pixelBuffer);
每個
AVAssetWriterInputPixelBufferAdaptor都包含一個assetWriterInput,用于接收緩沖區(qū)中的數(shù)據(jù),并且AVAssetWriterInput有一個很重要的屬性readyForMoreMediaData,來標識現(xiàn)在緩沖區(qū)中的數(shù)據(jù)是否已經(jīng)處理完成。通過判斷這個屬性,我們可以向
AVAssetWriterInputPixelBufferAdaptor中添加數(shù)據(jù)(appendPixelBuffer:)以進行處理。
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ù)據(jù)的采樣
第一步:創(chuàng)建AVAssetWriter對象,傳入生成視頻的路徑和格式
AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outFilePath] fileType:AVFileTypeMPEG4 error:&outError];
第二步:設置輸出視頻相關(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)建一個處理視頻樣本時專用的適配器對象,這個類在附加被包裝為CVPixelBufferRef對象的視頻樣本時能提供最優(yōu)性能。如果想要將CVPixelBufferRef類型對象添加到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ù)
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開始進行寫入流程
[assetWriter startSessionAtSourceTime:kCMTimeZero];
3.2 轉(zhuǎn)場切換效果中的圖片處理
上面介紹了音視頻合成的大致流程,但是核心的部分是在于我們在合成視頻時,如何去寫入第一張和第二張圖片展示間隙中的切換過程效果。這個時候就得引入GPUImage這個底層框架,而GPUImage是iOS端對OpenGL的封裝。即我們通過繼承GPUImageFilter去實現(xiàn)自定義濾鏡,并重寫片元著色器的效果,通過如下代理回調(diào)得到這個過程中返回的一系列處理好的紋理樣本數(shù)據(jù)。
-(void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
然后轉(zhuǎn)換成相應的pixelBuffer數(shù)據(jù),通過調(diào)用appendPixelBuffer:添加到幀緩存中去,從而寫入到文件中。
-(BOOL)appendPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;
3.2.1 如何自定義濾鏡
在GPUImageFilter中默認的著色器程序比較簡單,只是簡單的進行紋理采樣,并沒有對像素數(shù)據(jù)進行相關(guān)操作。所以在自定義相關(guān)濾鏡的時候,我們通常需要自定義片段著色器的效果來處理紋理效果從而達到豐富的轉(zhuǎn)場效果。
我們通過繼承GPUImageFilter來自定義我們轉(zhuǎn)場效果所需的濾鏡,首先是創(chuàng)建一個濾鏡文件compositeImageFilter繼承于GPUImageFilter,然后重寫父類的方法去初始化頂點和片段著色器。
- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;
這個時候需要傳入所需的片元著色器代碼,那么怎么自定義GLSL文件呢,以下便是如何編寫具體的GLSL文件,即片元著色器實現(xiàn)代碼。傳入紋理的頂點坐標textureCoordinate、2張圖片的紋理imageTexture、imageTexture2,通過mix函數(shù)混合兩個紋理圖像,使用time在[0,1]之間不停變化來控制第二個圖片紋理混合的強弱變化從而實現(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中重點API
在GPUImageFilter中有三個最重要的API,GPUImageFilter會將接收到的幀緩存對象經(jīng)過特定的片段著色器繪制到即將輸出的幀緩存對象中,然后將自己輸出的幀緩存對象傳給所有Targets并通知它們進行處理。方法被調(diào)用的順序:
1)生成新的幀緩存對象
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
2)進行紋理的繪制
- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
3)繪制完成通知所有的target處理下一幀的紋理數(shù)據(jù)
- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
通過如上代理回調(diào)就可以得到這個過程中返回的一系列處理好的紋理樣本數(shù)據(jù)。
按照方法調(diào)用順序,我們一般先重寫newFrameReadyAtTime方法,構(gòu)建最新的頂點坐標,生成新的幀緩存對象。
- (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]];
}
然后在這個方法中調(diào)用
renderToTextureWithVertices去繪制所需的紋理,并獲取到最終的幀緩存對象。以下是部分核心代碼:
- (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的寫入
在上述的處理過程當中,我們便獲取到了所需的幀緩存樣本數(shù)據(jù)pixel_buffer。而這個數(shù)據(jù)便是合成轉(zhuǎn)場切換過程中的數(shù)據(jù),我們把它進行寫入,自此便完成了第一張和第二張圖片轉(zhuǎn)場效果效果的寫入。待轉(zhuǎn)場效果寫入之后,我們便可按照此流程根據(jù)時間的進度寫入第二張圖片以及后續(xù)的第二張圖片和第三張圖片的轉(zhuǎn)場效果。依此類推,一直到寫完所有的圖片。
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;
}
//確定寫操作是否已完成、失敗或已取消
if ([self.videoPixelBufferAdaptor appendPixelBuffer:pixelBufferwithPresentationTime:frameTime]) {
NSLog(@"%f", CMTimeGetSeconds(frameTime));
}
}else {
NSLog(@"status:%d", self.assetWriter.status);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
以上便是在iOS端處理音視頻合成的具體步驟,難點在于如何使用GPUImage去實現(xiàn)復雜的轉(zhuǎn)場效果并將其寫到到容器中。
本文介紹了音視頻相關(guān)的基本知識,讓大家對音視頻的關(guān)鍵概念有了一些理解。然后分別介紹了Android和iOS這兩個移動平臺音視頻編解碼API,利用這些平臺自帶的API,我們可以將OpenGL渲染的畫面編碼成音視頻文件。鑒于篇幅限制,文中的流程只截取了部分關(guān)鍵步驟的代碼,歡迎大家來交流音視頻相關(guān)的知識。
作者簡介
jzg,攜程資深前端開發(fā)工程師,專注Android開發(fā);
zx,攜程高級前端開發(fā)工程師,專注iOS開發(fā);
zcc,攜程資深前端開發(fā)工程師,專注iOS開發(fā)。
熱門資訊
想了解動畫制作和影視特效的區(qū)別嗎?本文將帶您深入探討動畫制作和影視特效之間的關(guān)系,幫助你更好地理解這兩者的差異。
想知道快影、剪映、快剪輯這三款軟件哪個更適合小白?看看這篇對比評測,帶你了解這三款軟件的功能和特點,快速選擇適合自己的視頻剪輯軟件。
剪映專業(yè)版新增全局預覽縮放功能,可以輕松放大或縮小時間軌道。學習如何使用時間線縮放功能,提升剪輯效率。
4. 豆瓣8.3《鐵皮鼓》|電影符號學背后的視覺盛宴、社會隱喻主題
文|悅兒(叮咚,好電影來了!)《鐵皮鼓》是施隆多夫最具代表性的作品,影片于... 分析影片的社會隱喻主題;以及對于普通觀眾來說,它又帶給我們哪些現(xiàn)實啟發(fā)...
5. 從宏觀蒙太奇思維、中觀敘事結(jié)構(gòu)、微觀剪輯手法解讀《花樣年華》
中觀層面完成敘事結(jié)構(gòu)、以及微觀層面的剪輯手法,3個層次來解讀下電影《花樣年華》的蒙太奇魅力。一、 宏觀層面:運用蒙太奇思維構(gòu)建電影劇本雛形。蒙...
6. 15種電影剪輯/轉(zhuǎn)場藝術(shù),賦予影片絕妙魅力
15種電影剪輯/轉(zhuǎn)場手法,讓影片更吸引眼球!回顧電影中豐富多樣的專場技巧,比如瞬間從一個場景中變換到空中... 現(xiàn)在是測試技術(shù)的時候了!以下是一些常見剪輯手法,讓你觀影過程更加華麗動人!
本文介紹了十款強大的PR視頻剪輯插件,幫助提升視頻剪輯效率,提高創(chuàng)作品質(zhì),并降低創(chuàng)作難度。
1、每個切點需要理由和動機 很剪輯師認為,賦予每一個切點動機是非常困難的。很多...
電影創(chuàng)作者可以通過表意、造型、畫面展示等元素對隱身性的含義進行隱喻,打... 電影和夢境都具有普遍性的象征意義,夢境中的元素能夠代表人內(nèi)心的欲望,...
10. 干貨丨真正厲害的剪輯師都喜歡用這九大“技巧轉(zhuǎn)場”
想學習剪輯技術(shù)轉(zhuǎn)場的方法嗎?了解這九種技巧轉(zhuǎn)場方法,提升影片藝術(shù)感染力,讓你的視頻作品更加生動有趣!
同學您好!