通常我們在iOS(或Android)上通過OpenGl ES來播放視頻時,除了需要畫面能夠正常播放外,可能還有一些其他的需求,比如增加濾鏡、調整色值、畫面進行縮放等等各種各樣的需求。當然,不管是什么樣的需求,總是離不開對于著色器、紋理坐標、頂點坐標扽等的操作。今天,我們就來說一說,如何對正在播放的視頻進行縮放。
可能有一部分同學會說,那我們直接更改畫面的圖層Frame,進行放大、縮小不就好了?如果需要拖動再加上ScrolView不就好了?
理論上來說,這樣確實可以近似實現我們的需求,但是,當你真正了解了視頻畫面的播放、渲染的流程后,就不會這樣認為了。
1、通過改變圖層Frame來實現為什么不行?
首先,我們可以來看一下為什么通過直接操作視頻畫面圖層的大小來實現畫面的縮放是不可以的(或者說是不合理的)。
正常情況下,視頻畫面圖層剛好鋪滿整額窗口,渲染區域正好是整個圖層的大小。而當不斷放大圖層Frame時,渲染的區域則也會對應的變大。如上圖,放大后,實際上是把整個圖層放大了,我們能看的區域始終是窗口的大小,而渲染的區域卻會隨Frame的變大而變大,屏幕外的區域雖然我們看不到,但是還是會去渲染的。
我們都知道視頻最終展現在界面上,是需要CPU、GPU共同處理數據、經過渲染、提交到屏幕顯示的,我們需要顯示的區域越大,則對于CPU、GPU的壓力會越大,當到達他們能夠處理的上限后,肯定就會出現異常。
如下代碼主要是創建緩沖區,并指定渲染圖層:
? ?//創建幀緩沖區、渲染緩沖區并進行綁定
? ?glGenFramebuffers(1, &_framebuffer);
? ? glGenRenderbuffers(1, &_renderBuffer);
? ? //綁定
? ? glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
? ? glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
? ? //指定當前需要顯示畫面的圖層,如果改變圖層的大小,則一直會觸發并進入此方法,并更新需要渲染的區域
? ? if (![_glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer]) {
? ? ? ? NSLog(@"attach渲染緩沖區失敗");
? ? }
? ? glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
? ? if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
? ? ? ? NSLog(@"創建緩沖區錯誤 0x%x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
? ? }
當我們的layer的frame不斷變大到一定程度后,到達內存的瓶頸后(這里使用iPhoneX全屏播放,放大3倍左右就到大瓶頸了),在這里創建緩沖區會直接失敗(這里使用的OpenGl ES 2,如果是OpenGl ES 3則會直接Crash),則畫面會直接靜止,不會繼續播放了。
所以說,通過直接改變圖層Frame的方式來實現縮放功能,不僅造成了對資源的浪費,而且對放大的倍數也有限制,是不合理的。
重點來了........
那如何才能行呢?如何才能實時去縮放畫面還能不造成對資源的浪費呢?能否無限制的放大畫面呢?
接下來,我們來看看如果通過OpenGl來實現畫面的縮放、拖動
2、通過OpenGl實現視頻畫面的縮放、拖動
我們要做到不浪費資源、用戶看到的窗口是多大哪我們就渲染多大的區域,那圖層的layer的Frame就基本不變,只需要將窗口中畫面不同的區域進行放大即可。
實現原理,根據放大倍數的不同、顯示的畫面位置不同,在渲染畫面的窗口中映射到不同的區域來顯示畫面,通過上層算法來計算映射坐標、區域等參數。由于需要將視頻窗格的坐標系數據映射到OpenGl窗口中,所以需要進行坐標轉換。
1、在此之前,先說明下OpenGl的坐標系,這里主要說2D下的坐標系數據,紋理坐標、頂點坐標。我們都知道UIKit的坐標系,(0,0)點在屏幕左上角,而OpenGl的頂點坐標系的原點(0,0)在屏幕中央,(-1,-1)在左下角,(1,1)在右上角,如圖: ?
? ? ? ?1) 該坐標系為頂點坐標系,代表畫面在窗格中顯示的區域,一般頂點坐標系數組固定為?
? ? ? ? {?
? ? ? ? ? ? ? ? -1.0f, 1.0f,?
? ? ? ? ? ? ? ? 1.0f, 1.0f,?
? ? ? ? ? ? ? ? -1.0f, -1.0f,?
? ? ? ? ? ? ? ? 1.0f, -1.0f,?
? ? ? ? }?
分別表示左上、右上、左下、右下角的頂點坐標。?
? ? ? ? 2)接下來,就是紋理坐標系,它的(0,0)點在屏幕的左下角,剛好和UIKit下坐標系上下相反,坐標為?
? ? ? ? {?
? ? ? ? ? ? ? ? 0.0f, 1.0f,?
? ? ? ? ? ? ? ? 1.0f, 1.0f,?
? ? ? ? ? ? ? ? 0.0f, 0.0f,?
? ? ? ? ? ? ? ? 1.0f, 0.0f,?
? ? ? ? }?
同樣,分別表示左上、右上、左下、右下角的紋理坐標。?
2、放大局部畫面的話,實際上是改變畫面在紋理坐標系中的映射位置。
可以想象下,未放大時,窗口剛好和紋理坐標系大小重疊,當需要放大畫面時,將窗口映射縮小到紋理坐標系內不同的位置,即可實現局部畫面放大到整個窗口上。如圖:
可以看到,通過上面四次放大,窗口聚焦到了藍色區域(整個灰色區域可以看作是紋理坐標區域),而窗口的大小實際上并未改變,結果就是將藍色區域放大到了整個窗口上,這就達到了放大的效果。在這個過程中我們實際上是改變了紋理坐標的值,而實際上最終渲染的區域也是我們指定的區域,因此也不會出現資源浪費的問題,后面會詳細說明。
通過上面的圖,我們也可以聯想到,如果放大2倍(比如畫面中心點,即紋理坐標區域中心點(0.5,0.5)不變的情況下),那么紋理坐標的值就變成了
{
0.25f, 0.75f,
0.75f, 0.75f,
0.25f, 0.25f,
0.75f, 0.25f
}
所以,若放大倍數是scale,則半屏寬度、高度,可以用一下公式得到:
float deltaX = (1.0f / scale) / 2;
接著,如果我們知道顯示在屏幕窗口的區域的中心點,實際在紋理坐標系中的位置(x, y)的話,則可以通過以下代碼來得到紋理坐標:
/**
?設置放大比例,x,y 是希望在屏幕上放大的區域的中心點
?**/
- (void)scaleWithScaleRatio:(GLfloat)scaleRatiox:(GLfloat)xy:(GLfloat)y//設置放大比例
{
? ? UVILog(@"scaleRation:%f ,x=%f,y=%f", scaleRatio, x, y);
? ? centerPoint.x = x;
? ? centerPoint.y = y;
? ? GLfloatcoord[8];
? ? if (scaleRatio <=1.0f) {
? ? ? ? centerPoint.x=0.5f;
? ? ? ? centerPoint.y=0.5f;
? ? ? ? UVDLog(@"scaleRatio==1.0f");
? ? ? ? memcpy(coordVertices, coordVertices_init, sizeof(coord));
? ? ? ? //? ? 更新放大區域
? ? ? ? glVertexAttribPointer(ATTRIB_TEXTURE,2,GL_FLOAT,0,0,coordVertices);
? ? ? ? glEnableVertexAttribArray(ATTRIB_TEXTURE);
? ? ? ? mScaleRatio=1.0f;
? ? ? ? return; //注意,因為此時給了初始值,不需要再調用SetPosition了,直接返回
? ? } else {
? ? ? ? mScaleRatio= scaleRatio;
? ? ? ? //計算比例關系
? ? ? ? floatdeltaX = (1.0f/ scaleRatio) /2;
? ? ? ? floatdeltaY = (1.0f/ scaleRatio) /2;
? ? ? ? //計算四個點的坐標,給定的點是中心點(x, y),
? ? ? ? floatleftX, leftY;
? ? ? ? leftX = x - deltaX;//左上角X
? ? ? ? leftY = y + deltaY;//左上角Y
? ? ? ?//處理邊界值
? ? ? ? if(leftX <0.0f) {
? ? ? ? ? ? //X,Y 太靠左,向右移
? ? ? ? ? ? leftX =0.0f;
? ? ? ? }
? ? ? ? elseif((x + deltaX? )>1.0f) {
? ? ? ? ? ? //太靠右,向左移。
? ? ? ? ? ? leftX =1.0f-2*deltaX;
? ? ? ? }
? ? ? ? if(leftY >1.0f) {
? ? ? ? ? ? //Y 太靠上
? ? ? ? ? ? leftY =1.0f;
? ? ? ? }
? ? ? ? elseif((y - deltaY ) <0.0f) {
? ? ? ? ? ? //太靠下
? ? ? ? ? ? leftY =0.0f+2*deltaY;
? ? ? ? }
? ? ? ? coord[0] = leftX;//左上角
? ? ? ? coord[1] = leftY;
? ? ? ? coord[2] = leftX +2*deltaX;//右上角
? ? ? ? coord[3] = leftY;
? ? ? ? coord[4] = leftX;//左下角
? ? ? ? coord[5] = leftY -2*deltaY;
? ? ? ? coord[6] = leftX +2*deltaX;//右下角
? ? ? ? coord[7] = leftY -2*deltaY;
? ? ? ? for(inti=0;i<8;i++)
? ? ? ? {
? ? ? ? ? ? if(coord[i]<0.0f)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? coord[i]=0.0f;
? ? ? ? ? ? }
? ? ? ? ? ? elseif(coord[i]>1.0f)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? coord[i]=1.0f;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? NSLog(@"scaleRatio:%f ,leftX=%f,leftY=%f,deltaX:%f,deltaY:%f", scaleRatio,leftX,leftY,deltaX,deltaY);
? ? }
? ? [self setPositionWithCoord:coord];
}
- (void)setPositionWithCoord:(GLfloat *)coord {
? ? int length = 8; //(int)sizeof(coordVertices)/GLfloat;
? ? if(coord !=NULL) {
? ? ? ? for(inti =0; i < length; i++) {
? ? ? ? ? ? GLfloatvalue = *(coord);
? ? ? ? ? ? if(i %2==1) {
? ? ? ? ? ? ? ? //因為紋理坐標系與視頻UIKit坐標系上下相反的對應關系,需作處理
? ? ? ? ? ? ? ? value =1- value;
? ? ? ? ? ? }
? ? ? ? ? ? coordVertices[i] = value;
? ? ? ? ? ? coord++;
? ? ? ? }
? ? }
? ? //? ? 更新放大區域
? ? glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, coordVertices);
? ? glEnableVertexAttribArray(ATTRIB_TEXTURE);
}
上面就是如何直接通過OpenGl來放大、縮小畫面,而拖動畫面,則可以直接改變上述的(x, y)的坐標即可
?3、OpenGl中放大原理,通過放大倍數、畫面中心坐標三個參數實現畫面放大:?
? ? ? ? 1)將窗格畫面的中心點(即UIKit中的坐標(x,y))轉化映射到(0,1)坐標系中,定義為(glx,gly)。?
? ? ? ? 2)計算半屏寬、高。定義放大倍數a,則(1/a)/2,即為半屏寬、高。?
? ? ? ? 3)通過中心點(glx,gly),加、減對應的半屏寬、高,則可以獲得紋理坐標。將此坐標綁定到頂點坐標系中,則實現了畫面的放大、縮小?
(注意:紋理坐標需要處理邊界數據;y軸的坐標UIKit和OpenGl相反,需進行處理,如上述方法- (void)setPositionWithCoord:)?
?4、手勢縮放,通過UIKit坐標(x,y)實時計算紋理坐標系的中心點坐標(glx,gly):?
? ? ? ? 1)通過縮放中心點(x,y)在整個放大后的畫面中所占的比例來計算渲染中心點(glx,gly)時,在二次縮放后,縮放中心(x,y)可能變化很大,通過這種計算方式,會導致渲染中心點(glx,gly)的值發生大的變化,最終會表現出來畫面跳動、漂移的問題,即縮放過程不連續。?
? ? ? ? 為避免此問題,采用增量更新的方式,計算渲染中心點(glx,gly)。即每次縮放時,使用上一次的渲染中心點(glx,gly),加上本次縮放時,畫面中心的偏移量,來計算新的渲染中心點。由于每次的縮放間隔為0.05,因此增量很小,縮放過程很平緩。?
? ? ? ? 2)增量計算原理,手指在窗格上的縮放中心點為A點,B點為縮放前窗格中心點(在OpenGl坐標系中為B1點),縮放后,保持A點不變,此時窗格中心B點在OpenGl坐標系中對應B2點,(B2-B1)即為一次縮放的坐標增量,加上(glx,gly)就得到了新的OpenGl中心點?
(注:具體的算法詳見代碼)?
?5、數字放大后,拖動畫面實時計算新的渲染中心點(glx,gly):?
? ? ? ? 1)實現方式和上述類似,通過計算在OpenGl中的中心點坐標偏移量,加上拖動前的渲染中心點(glx,gly),得到最新的中心點?
? ? ? ? 2)例如,x方向偏移量為dx,窗格寬為width,放大倍數為a,在OpenGl中的偏移量為(dx/(width*a)),則glx -=(dx/(width*a))。同理,可計算gly(注意:glx和gly的坐標方向相反)?
? ? ? ? 綜述,數字放大實現的核心處理如上,詳細算法見代碼。?
? ? ? ? 另外,關于數字放大,主要關注縮放、拖動過程的連續性、平緩性,尤其是二次縮放、拖動。
通過這種方式放大畫面后,將不受到GPU的限制,因為我們渲染的Frame始終未變,只要我們的畫質夠高,則可以放大任意倍數,幾十倍都OK。
核心算法詳見GitHub代碼: