如果我們使用AVPlayer及AVPlayerLayer進行視頻播放的話,那們我們可以使用AVPlayerLayer.videoGravity來控件畫面的顯示比例(Resize, ResizeAspect, ResizeAspectFill)。但是如果我們使用Metal進行視頻渲染的放要如何實現畫面比例呢?
其實我們可以通過設置Metal的View Point來實現:
畫面比例如果使用AVSampleBufferDisplayLayer有一個videoGravity屬性:
@property(copy) AVLayerVideoGravity videoGravity;
使用系統播放器時,AVPlayerLayer也有一個videoGravity屬性:
@property(copy) AVLayerVideoGravity videoGravity;
但在使用Metal進行渲染時MTLLayer並沒有類似的屬性,只有在父類CALayer有一個contentsGravity屬性:
@property(copy) CALayerContentsGravity contentsGravity;
但CALayer的contentsGravity屬性並不能讓MTLLayer上渲染的視頻產生畫面比例的效果。我們需要使用MTLRenderCommandEncoder的以下設置View point的接口來實現:
- (void)setViewport:(MTLViewport)viewport;
MTLViewport定義:
typedef struct { double originX, originY, width, height, znear, zfar; } MTLViewport;
Metal是可以進行3D渲染的,在我們的視頻渲染中只使用到了2D,因此這個znear和zfar直接取-1和1就好,對於2D渲染,默認MTViewpoint為:
(MTLViewport){0.0, 0.0, drawableSize.width, drawableSize.height, -1.0, 1.0 }];
以上view point會使視頻紋理縮放填滿整個畫布,而不保持視頻原分辨率比例 。
在我們下面通過設置view point方法來實現畫面比例前,有如下條件:
// 視頻寬高 CGFloat videoWidth = textureSize.width; CGFloat videoHeight = textureSize.height; // 視頻高寬比 CGFloat videoScale = videoHeight/videoWidth; // 畫面同寬比 CGFloat canvaseScale = (CGFloat)(drawableSize.height) / (CGFloat)(drawableSize.width); // 視頻實際顯示的寬高,即在view point中使用的寬高 CGFloat videoDisplayWidth = drawableSize.width; CGFloat videoDisplayHeight = drawableSize.height;
視頻的畫面比例通常我們需要實現三種情況:
kCAGravityResize:
視頻寬高隨畫布寬高進行縮放,將畫布填滿,不考慮視頻本身的比例,圖像會變形。
Resize的實現很簡單,不用特殊設置ViewPoint默認視頻就是按Layer進行拉伸變形填滿整個Metal Layer的。由於要實現其它兩種畫面比例,設置viewPoint如下:
[encoder setViewport:(MTLViewport){0.0, 0.0, videoDisplayWidth, videoDisplayHeight, -1.0, 1.0 }];
kCAGravityResizeAspect:
將視頻進行等比縮放,當視頻相對畫布較大的邊剛好和畫布一樣時不再縮放,如果視頻比例與畫面比例不相等時,上下或者左右會出現黑邊。
分兩種情況。
1. 如果canvaseScale > videoScale
即畫布相比例對視頻比例,更高,那么當視頻等比縮放到和畫布的寬度一樣時,畫面上下就會有黑邊,我們使上下的黑邊一樣,如圖:
視頻顯示寬高分別為:
videoDisplayWidth = drawableSize.width; videoDisplayHeight = drawableSize.width * (videoHeight/videoWidth);
此時的高度沒有畫布高,MTLViewport的originX為0,originY應該比0大,視頻向上偏移,使下面有黑邊,偏移的量正好是畫布高度減去縮放后視頻顯示高度的差再除以2,因此viewPoint設置如下:
[encoder setViewport:(MTLViewport){0.0, (drawableSize.height - videoDisplayHeight)/2, videoDisplayWidth, videoDisplayHeight, -1.0, 1.0 }];
2. 如果canvaseScale <= videoScale
即畫布相對視頻比例,更寬,那么當視頻等比縮放到與畫布高度一樣時,畫面的左右就會有黑邊,我們使左右黑邊一樣,如圖:
視頻顯示寬高分別為:
videoDisplayWidth = drawableSize.height * (videoWidth/videoHeight); videoDisplayHeight = drawableSize.height;
此時的寬沒有畫布寬,MTLViewpoint的originY為0,orignX應該比0大,視頻向右偏移,使左邊有黑邊,偏移的量正好是畫布寬減去縮放后視頻顯示寬的差再除以2,因此viewPoint設置如下:
[encoder setViewport:(MTLViewport){(drawableSize.width - videoDisplayWidth)/2, 0, videoDisplayWidth, videoDisplayHeight, -1.0, 1.0 }];
kCAGravityResizeAspectFill
將視頻在kCAGravityResizeAspect的基礎上進行等比放大,當上下或者左右的黑邊剛好消失時停止。這樣上下或者左右的畫面會有一部分在畫布外面看不見。
同樣分兩種情況討論。
3. 如果canvaseScale > videoScale
即畫布相對視頻比例,更高,那么當視頻等比縮放到和畫布的高一樣時,視頻的左右就會有一部分跑到畫布外面去,我們使視頻居中,左右跑出畫布的視頻寬度一樣,如圖:
視頻顯示寬高分別為:
videoDisplayWidth = (videoWidth/videoHeight) * drawableSize.height; videoDisplayHeight = drawableSize.height;
此時視頻的寬videoDisplayWidth已經比畫布的寬drawableSize.width要大了,為了居中,MTLViewpoint的orignX應該比0小,使視頻畫面向畫布左偏移移。因此viewPoint設置如下:
[encoder setViewport:(MTLViewport){(drawableSize.width - videoDisplayWidth)/2, 0, videoDisplayWidth, videoDisplayHeight, -1.0, 1.0 }];
4. 如果canvaseScale <= videoScale
即畫布相對視頻比例,更寬,那么當視頻等比縮放到和畫面一樣寬時,視頻的上下就會有一部分跑到畫布的外面去,我們要使視頻居中,上下移出畫布的視頻高度應該一樣,如下圖:
視頻顯示寬高分別為:
videoDisplayWidth = drawableSize.width; videoDisplayHeight = (videoHeight/videoWidth)*drawableSize.width;
此時視頻的高videoDisplayHeitht已經比畫面的高drawableSize.height要大了,為了居中,MTLViewpoint的originY應該比0小,使視頻的畫面向畫布下偏移。因此viewPoint設置如下:
[encoder setViewport:(MTLViewport){0, (drawableSize.height - videoDisplayHeight)/2, videoDisplayWidth, videoDisplayHeight, -1.0, 1.0 }];