渲染是音视频技术栈相关的一个非常重要的方向,视频图像在设备上的展示、各种流行的视频特效都离不开渲染技术的支持。
在 RenderDemo 这个工程示例系列,我们将为大家展示一些渲染相关的 Demo,来向大家介绍如何在 iOS/Android 平台上手一些渲染相关的开发。
这里是第二篇:用 OpenGL 渲染视频。我们分别在 iOS 和 Android 实现了用 OpenGL 渲染视频数据的 Demo。在本文中,包括如下内容:
- 1)iOS 视频 OpenGL 渲染 Demo;
- 2)Android 视频 OpenGL 渲染 Demo;
- 3)详尽的代码注释,帮你理解代码逻辑和原理。
本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
1、iOS Demo
其实我们在之前的 iOS 视频采集的 Demo 中已经使用了系统的 API AVCaptureVideoPreviewLayer 来实现了视频数据的渲染,不过现在我们准备深入渲染的细节,所以我们这里会使用 OpenGL 来自己实现渲染模块替换掉 AVCaptureVideoPreviewLayer。
1.1、视频采集模块
视频采集模块与 iOS 视频采集的 Demo 中讲到的一致,这里就不再细讲,只贴一下主要代码:
首先,实现一个 KFVideoCaptureConfig 类用于定义视频采集参数的配置。
KFVideoCaptureConfig.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KFVideoCaptureMirrorType) {
KFVideoCaptureMirrorNone = 0,
KFVideoCaptureMirrorFront = 1 << 0,
KFVideoCaptureMirrorBack = 1 << 1,
KFVideoCaptureMirrorAll = (KFVideoCaptureMirrorFront | KFVideoCaptureMirrorBack),
};
@interface KFVideoCaptureConfig : NSObject
@property (nonatomic, copy) AVCaptureSessionPreset preset; // 视频采集参数,比如分辨率等,与画质相关。
@property (nonatomic, assign) AVCaptureDevicePosition position; // 摄像头位置,前置/后置摄像头。
@property (nonatomic, assign) AVCaptureVideoOrientation orientation; // 视频画面方向。
@property (nonatomic, assign) NSInteger fps; // 视频帧率。
@property (nonatomic, assign) OSType pixelFormatType; // 颜色空间格式。
@property (nonatomic, assign) KFVideoCaptureMirrorType mirrorType; // 镜像类型。
@end
NS_ASSUME_NONNULL_END
KFVideoCaptureConfig.m
#import "KFVideoCaptureConfig.h"
@implementation KFVideoCaptureConfig
- (instancetype)init {
self = [super init];
if (self) {
_preset = AVCaptureSessionPreset1920x1080;
_position = AVCaptureDevicePositionFront;
_orientation = AVCaptureVideoOrientationPortrait;
_fps = 30;
_mirrorType = KFVideoCaptureMirrorFront;
// 设置颜色空间格式,这里要注意了:
// 1、一般我们采集图像用于后续的编码时,这里设置 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange 即可。
// 2、如果想支持 HDR 时(iPhone12 及之后设备才支持),这里设置为:kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange。
_pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
}
return self;
}
@end
接下来,我们实现一个 KFVideoCapture 类来实现视频采集。
KFVideoCapture.h
#import <Foundation/Foundation.h>
#import "KFVideoCaptureConfig.h"
NS_ASSUME_NONNULL_BEGIN
@interface KFVideoCapture : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFVideoCaptureConfig *)config;
@property (nonatomic, strong, readonly) KFVideoCaptureConfig *config;
@property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 视频采集数据回调。
@property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 视频采集会话错误回调。
@property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 视频采集会话初始化成功回调。
- (void)startRunning; // 开始采集。
- (void)stopRunning; // 停止采集。
- (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切换摄像头。
@end
NS_ASSUME_NONNULL_END
KFVideoCapture.m
#import "KFVideoCapture.h"
#import <UIKit/UIKit.h>
@interface KFVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, strong, readwrite) KFVideoCaptureConfig *config;
@property (nonatomic, strong, readonly) AVCaptureDevice *captureDevice; // 视频采集设备。
@property (nonatomic, strong) AVCaptureDeviceInput *backDeviceInput; // 后置摄像头采集输入。
@property (nonatomic, strong) AVCaptureDeviceInput *frontDeviceInput; // 前置摄像头采集输入。
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoOutput; // 视频采集输出。
@property (nonatomic, strong) AVCaptureSession *captureSession; // 视频采集会话。
@property (nonatomic, strong, readwrite) AVCaptureVideoPreviewLayer *previewLayer; // 视频预览渲染 layer。
@property (nonatomic, assign, readonly) CMVideoDimensions sessionPresetSize; // 视频采集分辨率。
@property (nonatomic, strong) dispatch_queue_t captureQueue;
@end
@implementation KFVideoCapture
#pragma mark - Property
- (AVCaptureDevice *)backCamera {
return [self cameraWithPosition:AVCaptureDevicePositionBack];
}
- (AVCaptureDeviceInput *)backDeviceInput {
if (!_backDeviceInput) {
_backDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self backCamera] error:nil];
}
return _backDeviceInput;
}
- (AVCaptureDevice *)frontCamera {
return [self cameraWithPosition:AVCaptureDevicePositionFront];
}
- (AVCaptureDeviceInput *)frontDeviceInput {
if (!_frontDeviceInput) {
_frontDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self frontCamera] error:nil];
}
return _frontDeviceInput;
}
- (AVCaptureVideoDataOutput *)videoOutput {
if (!_videoOutput) {
_videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[_videoOutput setSampleBufferDelegate:self queue:self.captureQueue]; // 设置返回采集数据的代理和回调。
_videoOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(_config.pixelFormatType)};
_videoOutput.alwaysDiscardsLateVideoFrames = YES; // YES 表示:采集的下一帧到来前,如果有还未处理完的帧,丢掉。
}
return _videoOutput;
}
- (AVCaptureSession *)captureSession {
if (!_captureSession) {
AVCaptureDeviceInput *deviceInput = self.config.position == AVCaptureDevicePositionBack ? self.backDeviceInput : self.frontDeviceInput;
if (!deviceInput) {
return nil;
}
// 1、初始化采集会话。
_captureSession = [[AVCaptureSession alloc] init];
// 2、添加采集输入。
for (AVCaptureSessionPreset selectPreset in [self sessionPresetList]) {
if ([_captureSession canSetSessionPreset:selectPreset]) {
[_captureSession setSessionPreset:selectPreset];
if ([_captureSession canAddInput:deviceInput]) {
[_captureSession addInput:deviceInput];
break;
}
}
}
// 3、添加采集输出。
if ([_captureSession canAddOutput:self.videoOutput]) {
[_captureSession addOutput:self.videoOutput];
}
// 4、更新画面方向。
[self _updateOrientation];
// 5、更新画面镜像。
[self _updateMirror];
// 6、更新采集实时帧率。
[self.captureDevice lockForConfiguration:nil];
[self _updateActiveFrameDuration];
[self.captureDevice unlockForConfiguration];
// 7、回报成功。
if (self.sessionInitSuccessCallBack) {
self.sessionInitSuccessCallBack();
}
}
return _captureSession;
}
- (AVCaptureVideoPreviewLayer *)previewLayer {
if (!_captureSession) {
return nil;
}
if (!_previewLayer) {
// 初始化预览渲染 layer。这里就直接用系统提供的 API 来渲染。
_previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
[_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
return _previewLayer;
}
- (AVCaptureDevice *)captureDevice {
// 视频采集设备。
return (self.config.position == AVCaptureDevicePositionBack) ? [self backCamera] : [self frontCamera];
}
- (CMVideoDimensions)sessionPresetSize {
// 视频采集分辨率。
return CMVideoFormatDescriptionGetDimensions([self captureDevice].activeFormat.formatDescription);
}
#pragma mark - LifeCycle
- (instancetype)initWithConfig:(KFVideoCaptureConfig *)config {
self = [super init];
if (self) {
_config = config;
_captureQueue = dispatch_queue_create("com.KeyFrameKit.videoCapture", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Public Method
- (void)startRunning {
typeof(self) __weak weakSelf = self;
dispatch_async(_captureQueue, ^{
[weakSelf _startRunning];
});
}
- (void)stopRunning {
typeof(self) __weak weakSelf = self;
dispatch_async(_captureQueue, ^{
[weakSelf _stopRunning];
});
}
- (void)changeDevicePosition:(AVCaptureDevicePosition)position {
typeof(self) __weak weakSelf = self;
dispatch_async(_captureQueue, ^{
[weakSelf _updateDeveicePosition:position];
});
}
#pragma mark - Private Method
- (void)_startRunning {
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (status == AVAuthorizationStatusAuthorized) {
if (!self.captureSession.isRunning) {
[self.captureSession startRunning];
}
} else {
NSLog(@"没有相机使用权限");
}
}
- (void)_stopRunning {
if (_captureSession && _captureSession.isRunning) {
[_captureSession stopRunning];
}
}
- (void)_updateDeveicePosition:(AVCaptureDevicePosition)position {
// 切换采集的摄像头。
if (position == self.config.position || !_captureSession.isRunning) {
return;
}
// 1、切换采集输入。
AVCaptureDeviceInput *curInput = self.config.position == AVCaptureDevicePositionBack ? self.backDeviceInput : self.frontDeviceInput;
AVCaptureDeviceInput *addInput = self.config.position == AVCaptureDevicePositionBack ? self.frontDeviceInput : self.backDeviceInput;
if (!curInput || !addInput) {
return;
}
[self.captureSession removeInput:curInput];
for (AVCaptureSessionPreset selectPreset in [self sessionPresetList]) {
if ([_captureSession canSetSessionPreset:selectPreset]) {
[_captureSession setSessionPreset:selectPreset];
if ([_captureSession canAddInput:addInput]) {
[_captureSession addInput:addInput];
self.config.position = position;
break;
}
}
}
// 2、更新画面方向。
[self _updateOrientation];
// 3、更新画面镜像。
[self _updateMirror];
// 4、更新采集实时帧率。
[self.captureDevice lockForConfiguration:nil];
[self _updateActiveFrameDuration];
[self.captureDevice unlockForConfiguration];
}
- (void)_updateOrientation {
// 更新画面方向。
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo]; // AVCaptureConnection 用于把输入和输出连接起来。
if ([connection isVideoOrientationSupported] && connection.videoOrientation != self.config.orientation) {
connection.videoOrientation = self.config.orientation;
}
}
- (void)_updateMirror {
// 更新画面镜像。
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
if ([connection isVideoMirroringSupported]) {
if ((self.config.mirrorType & KFVideoCaptureMirrorFront) && self.config.position == AVCaptureDevicePositionFront) {
connection.videoMirrored = YES;
} else if ((self.config.mirrorType & KFVideoCaptureMirrorBack) && self.config.position == AVCaptureDevicePositionBack) {
connection.videoMirrored = YES;
} else {
connection.videoMirrored = NO;
}
}
}
- (BOOL)_updateActiveFrameDuration {
// 更新采集实时帧率。
// 1、帧率换算成帧间隔时长。
CMTime frameDuration = CMTimeMake(1, (int32_t) self.config.fps);
// 2、设置帧率大于 30 时,找到满足该帧率及其他参数,并且当前设备支持的 AVCaptureDeviceFormat。
if (self.config.fps > 30) {
for (AVCaptureDeviceFormat *vFormat in [self.captureDevice formats]) {
CMFormatDescriptionRef description = vFormat.formatDescription;
CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(description);
float maxRate = ((AVFrameRateRange *) [vFormat.videoSupportedFrameRateRanges objectAtIndex:0]).maxFrameRate;
if (maxRate >= self.config.fps && CMFormatDescriptionGetMediaSubType(description) == self.config.pixelFormatType && self.sessionPresetSize.width * self.sessionPresetSize.height == dims.width * dims.height) {
self.captureDevice.activeFormat = vFormat;
break;
}
}
}
// 3、检查设置的帧率是否在当前设备的 activeFormat 支持的最低和最高帧率之间。如果是,就设置帧率。
__block BOOL support = NO;
[self.captureDevice.activeFormat.videoSupportedFrameRateRanges enumerateObjectsUsingBlock:^(AVFrameRateRange * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (CMTimeCompare(frameDuration, obj.minFrameDuration) >= 0 &&
CMTimeCompare(frameDuration, obj.maxFrameDuration) <= 0) {
support = YES;
*stop = YES;
}
}];
if (support) {
[self.captureDevice setActiveVideoMinFrameDuration:frameDuration];
[self.captureDevice setActiveVideoMaxFrameDuration:frameDuration];
return YES;
}
return NO;
}
#pragma mark - NSNotification
- (void)sessionRuntimeError:(NSNotification *)notification {
if (self.sessionErrorCallBack) {
self.sessionErrorCallBack(notification.userInfo[AVCaptureSessionErrorKey]);
}
}
#pragma mark - Utility
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
// 从当前手机寻找符合需要的采集设备。
NSArray *devices = nil;
NSString *version = [UIDevice currentDevice].systemVersion;
if (version.doubleValue >= 10.0) {
AVCaptureDeviceDiscoverySession *deviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position];
devices = deviceDiscoverySession.devices;
} else {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
#pragma GCC diagnostic pop
}
for (AVCaptureDevice *device in devices) {
if ([device position] == position) {
return device;
}
}
return nil;
}
- (NSArray *)sessionPresetList {
return @[self.config.preset, AVCaptureSessionPreset3840x2160, AVCaptureSessionPreset1920x1080, AVCaptureSessionPreset1280x720, AVCaptureSessionPresetLow];
}
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 向外回调数据。
if (output == self.videoOutput) {
if (self.sampleBufferOutputCallBack) {
self.sampleBufferOutputCallBack(sampleBuffer);
}
}
}
@end
上面是 KFVideoCapture 的实现。
1.2、视频渲染模块
1)渲染视图 KFOpenGLView
接下来,我们来用 OpenGL 实现一个支持视频数据渲染的 View,对应的接口如下:
KFOpenGLView.h
#import <UIKit/UIKit.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
#import "KFTextureFrame.h"
// 渲染画面填充模式。
typedef NS_ENUM(NSInteger, KFGLViewContentMode) {
// 自动填充满,可能会变形。
KFGLViewContentModeStretch = 0,
// 按比例适配,可能会有黑边。
KFGLViewContentModeFit = 1,
// 根据比例裁剪后填充满。
KFGLViewContentModeFill = 2
};
// 使用 OpenGL 实现渲染 View。
@interface KFOpenGLView : UIView
- (instancetype)initWithFrame:(CGRect)frame context:(nullable EAGLContext *)context;
@property (nonatomic, assign) KFGLViewContentMode fillMode; // 画面填充模式。
- (void)displayFrame:(nonnull KFTextureFrame *)frame; // 渲染一帧纹理。
@end
核心功能是提供了设置画面填充模式的接口和渲染一帧纹理的接口。下面是对应的实现:
KFOpenGLView.m
#import "KFOpenGLView.h"
#import <QuartzCore/QuartzCore.h>
#import <AVFoundation/AVUtilities.h>
#import <mach/mach_time.h>
#import <GLKit/GLKit.h>
#import "KFGLFilter.h"
#import "KFGLBase.h"
#import <GLKit/GLKit.h>
@interface KFOpenGLView() {
// The pixel dimensions of the CAEAGLLayer.
GLint _backingWidth;
GLint _backingHeight;
GLuint _frameBufferHandle;
GLuint _colorBufferHandle;
KFGLFilter *_filter;
GLfloat _customVertices[8];
}
@property (nonatomic, assign) CGSize currentViewSize; // 当前 view 大小。
@property (nonatomic, assign) CGSize frameSize; // 当前被渲染的纹理大小。
@end
@implementation KFOpenGLView
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame context:(nullable EAGLContext *)context{
if (self = [super initWithFrame:frame]) {
self.contentScaleFactor = [[UIScreen mainScreen] scale];
// 设定 layer 相关属性。
CAEAGLLayer *eaglLayer = (CAEAGLLayer *) self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking: @(NO),
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
_fillMode = KFGLViewContentModeFit;
// 设置当前 OpenGL 上下文,并初始化相关 GL 环境。
if (context) {
EAGLContext *preContext = [EAGLContext currentContext];
[EAGLContext setCurrentContext:context];
[self _setupGL];
[EAGLContext setCurrentContext:preContext];
} else {
NSLog(@"KFOpenGLView context nil");
}
}
return self;
}
- (void)layoutSubviews {
// 视图自动调整布局,同步至渲染视图。
[super layoutSubviews];
_currentViewSize = self.bounds.size;
}
- (void)dealloc {
if(_frameBufferHandle != 0){
glDeleteFramebuffers(1, &_frameBufferHandle);
}
if(_colorBufferHandle != 0){
glDeleteRenderbuffers(1, &_colorBufferHandle);
}
}
# pragma mark - OpenGL Setup
- (void)_setupGL {
// 1、申请并绑定帧缓冲区对象 FBO。
glGenFramebuffers(1, &_frameBufferHandle);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
// 2、申请并绑定渲染缓冲区对象 RBO。
glGenRenderbuffers(1, &_colorBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
// 3、将渲染图层(_eaglLayer)的存储绑定到 RBO。
[[EAGLContext currentContext] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
// 当渲染缓冲区 RBO 绑定存储空间完成后,可以通过 glGetRenderbufferParameteriv 获取渲染缓冲区的宽高,实际跟上面设置的 layer 的宽高一致。
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
// 4、将 RBO 绑定为 FBO 的一个附件。绑定后,OpenGL 对 FBO 的绘制会同步到 RBO 后再上屏。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
// 5、KFGLFilter 封装了 shader 的加载、编译和着色器程序链接,以及 FBO 的管理。这里用一个 Filter 来实现具体的渲染细节。
_filter = [[KFGLFilter alloc] initWithCustomFBO:YES vertexShader:KFDefaultVertexShader fragmentShader:KFDefaultFragmentShader]; // 这里 isCustomFBO 传 YES,表示直接用外部的 FBO(即上面创建的 FBO 对象 _frameBufferHandle)。vertexShader 和 fragmentShader 则都使用默认的。
__weak typeof(self) wself = self;
_filter.preDrawCallBack = ^(){
// 在渲染前回调中,关联顶点位置数据。通过渲染回调接口,可以在外部更新顶点数据。
__strong typeof(wself) sself = wself;
if (sself) {
glVertexAttribPointer([[sself->_filter getProgram] getAttribLocation:@"position"], 2, GL_FLOAT, 0, 0, sself->_customVertices);
}
};
}
- (void)_updaterVertices {
// 根据视频画面填充模式计算顶点数据。
float heightScaling = 1.0;
float widthScaling = 1.0;
if (!CGSizeEqualToSize(_currentViewSize, CGSizeZero) && !CGSizeEqualToSize(_frameSize, CGSizeZero)) {
CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(_frameSize, CGRectMake(0, 0, _currentViewSize.width, _currentViewSize.height));
switch (_fillMode) {
case KFGLViewContentModeStretch: {
widthScaling = 1.0;
heightScaling = 1.0;
break;
}
case KFGLViewContentModeFit: {
widthScaling = insetRect.size.width / _currentViewSize.width;
heightScaling = insetRect.size.height / _currentViewSize.height;
break;
}
case KFGLViewContentModeFill: {
widthScaling = _currentViewSize.height / insetRect.size.height;
heightScaling = _currentViewSize.width / insetRect.size.width;
break;
}
}
}
_customVertices[0] = -widthScaling;
_customVertices[1] = -heightScaling;
_customVertices[2] = widthScaling;
_customVertices[3] = -heightScaling;
_customVertices[4] = -widthScaling;
_customVertices[5] = heightScaling;
_customVertices[6] = widthScaling;
_customVertices[7] = heightScaling;
}
#pragma mark - OpenGLES Render
// 渲染一帧纹理。
- (void)displayFrame:(KFTextureFrame *)frame {
if (![EAGLContext currentContext] || !frame) {
return;
}
// 1、绑定 FBO、RBO 到 OpenGL 渲染管线。
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
// 2、设置视口大小为整个渲染缓冲区的区域。
glViewport(0, 0, _backingWidth, _backingHeight);
// 3、渲染传进来的一帧纹理。
KFTextureFrame *renderFrame = frame.copy; // 获取纹理。
_frameSize = renderFrame.textureSize; // 记录纹理大小。
// 将 GL 的坐标系(↑→)适配屏幕坐标系(↓→),生成新的 mvp 矩阵。
GLKVector4 scale = {1, -1, 1, 1};
renderFrame.mvpMatrix = GLKMatrix4ScaleWithVector4(GLKMatrix4Identity, scale);
[self _updaterVertices]; // 更新一下顶点位置数据。外部如何更改了画面填充模式会影响顶点位置。
[_filter render:renderFrame]; // 渲染。
// 4、把 RBO 的内容显示到窗口系统 (CAEAGLLayer) 中。
[[EAGLContext currentContext] presentRenderbuffer:GL_RENDERBUFFER];
// 5、将 FBO、RBO 从 OpenGL 渲染管线解绑。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
}
@end
相关的代码解释,我们已经在代码注释里进行了说明。这里面最关键部分是我们不再将所有的 OpenGL 代码都放在一个类里,而是根据功能模块进行了封装。在 KFOpenGLView 中,除了常规的 OpenGL 环境初始化,我们封装了一个 KFGLFilter 类实现 shader 的加载、编译和着色器程序链接,以及 FBO 的管理,并用一个 KFGLFilter 示例去完成具体的渲染细节。
2)渲染节点 KFGLFilter
KFGLFilter 的代码如下:
KFGLFilter.h
#import <Foundation/Foundation.h>
#import "KFGLFrameBuffer.h"
#import "KFGLProgram.h"
#import "KFTextureFrame.h"
NS_ASSUME_NONNULL_BEGIN
// KFGLFilter 封装了 shader 的加载、编译和着色器程序链接,以及 FBO 的管理
@interface KFGLFilter : NSObject
// KFGLFilter 初始化。
// 这里 isCustomFBO 传 YES,表示直接用外部的 FBO(即上面创建的 FBO 对象 _frameBufferHandle)。
- (instancetype)initWithCustomFBO:(BOOL)isCustomFBO vertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader;
- (instancetype)initWithCustomFBO:(BOOL)isCustomFBO vertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader textureAttributes:(KFGLTextureAttributes *)textureAttributes;
@property (nonatomic, copy) void (^preDrawCallBack)(void); // 渲染前回调。
@property (nonatomic, copy) void (^postDrawCallBack)(void); // 渲染后回调。
- (KFGLFrameBuffer *)getOutputFrameBuffer; // 获取内部的 FBO。
- (KFGLProgram *)getProgram; // 获取 GL 程序。
- (KFTextureFrame *)render:(KFTextureFrame*)frame; // 渲染一帧纹理。
// 设置 GL 程序变量值。
- (void)setIntegerUniformValue:(NSString *)uniformName intValue:(int)intValue;
- (void)setFloatUniformValue:(NSString *)uniformName floatValue:(float)floatValue;
@end
NS_ASSUME_NONNULL_END
从 KFGLFilter 的接口设计中我们可以看到主要提供了获取内部的 FBO、获取 GL 程序、设置 GL 程序变量值、渲染一帧纹理、渲染前回调、渲染后回调等接口。具体实现代码如下:
KFGLFilter.mm
#import "KFGLFilter.h"
#import <OpenGLES/ES2/glext.h>
@interface KFGLFilter() {
BOOL _mIsCustomFBO;
KFGLFrameBuffer *_mFrameBuffer;
KFGLProgram *_mProgram;
KFGLTextureAttributes *_mGLTextureAttributes;
int _mTextureUniform;
int _mPostionMatrixUniform;
int _mPositionAttribute;
int _mTextureCoordinateAttribute;
}
@end
@implementation KFGLFilter
- (instancetype)initWithCustomFBO:(BOOL)isCustomFBO vertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader {
return [self initWithCustomFBO:isCustomFBO vertexShader:vertexShader fragmentShader:fragmentShader textureAttributes:[KFGLTextureAttributes new]];
}
- (instancetype)initWithCustomFBO:(BOOL)isCustomFBO vertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader textureAttributes:(KFGLTextureAttributes *)textureAttributes {
self = [super init];
if (self) {
// 初始化。
_mTextureUniform = -1;
_mPostionMatrixUniform = -1;
_mPositionAttribute = -1;
_mTextureCoordinateAttribute = -1;
_mIsCustomFBO = isCustomFBO;
_mGLTextureAttributes = textureAttributes;
// 加载和编译 shader,并链接到着色器程序。
[self _setupProgram:vertexShader fragmentShader:fragmentShader];
}
return self;
}
- (void)dealloc {
if (_mFrameBuffer != nil) {
_mFrameBuffer = nil;
}
if (_mProgram != nil) {
_mProgram = nil;
}
}
- (KFGLFrameBuffer *)getOutputFrameBuffer {
// 当没有指定外部 FBO 时,内部会生成一个 FBO,这里返回的是内部的 FBO。
return _mFrameBuffer;
}
-(KFGLProgram *)getProgram {
// 返回 GL 程序。
return _mProgram;
}
- (void)setIntegerUniformValue:(NSString *)uniformName intValue:(int)intValue {
// 设置 GL 程序变量值。
if (_mProgram != nil) {
int uniforamIndex = [_mProgram getUniformLocation:uniformName];
[_mProgram use];
glUniform1i(uniforamIndex, intValue);
}
}
- (void)setFloatUniformValue:(NSString *)uniformName floatValue:(float)floatValue {
// 设置 GL 程序变量值。
if (_mProgram != nil) {
int uniforamIndex = [_mProgram getUniformLocation:uniformName];
[_mProgram use];
glUniform1f(uniforamIndex, floatValue);
}
}
- (void)_setupFrameBuffer:(CGSize)size {
// 如果指定使用外部的 FBO,则这里就直接返回。
if (_mIsCustomFBO) {
return;
}
// 如果没指定使用外部的 FBO,这里就再创建一个 FBO。
if (_mFrameBuffer == nil || _mFrameBuffer.getSize.width != size.width || _mFrameBuffer.getSize.height != size.height) {
if (_mFrameBuffer != nil) {
_mFrameBuffer = nil;
}
_mFrameBuffer = [[KFGLFrameBuffer alloc] initWithSize:size textureAttributes:_mGLTextureAttributes];
}
}
- (void)_setupProgram:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader {
// 加载和编译 shader,并链接到着色器程序。
if (_mProgram == nil) {
_mProgram = [[KFGLProgram alloc] initWithVertexShader:vertexShader fragmentShader:fragmentShader];
// 获取与 Shader 中对应参数的位置值:
_mTextureUniform = [_mProgram getUniformLocation:@"inputImageTexture"];
_mPostionMatrixUniform = [_mProgram getUniformLocation:@"mvpMatrix"];
_mPositionAttribute = [_mProgram getAttribLocation:@"position"];
_mTextureCoordinateAttribute = [_mProgram getAttribLocation:@"inputTextureCoordinate"];
}
}
- (KFTextureFrame *)render:(KFTextureFrame *)frame {
// 渲染一帧纹理。
if (frame == nil) {
return frame;
}
KFTextureFrame *resultFrame = frame.copy;
[self _setupFrameBuffer:frame.textureSize];
if (_mFrameBuffer != nil) {
[_mFrameBuffer bind];
}
if (_mProgram != nil) {
// 使用 GL 程序。
[_mProgram use];
// 清理窗口颜色。
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
// 激活和绑定纹理单元,并设置 uniform 采样器与之对应。
glActiveTexture(GL_TEXTURE1); // 在绑定纹理之前先激活纹理单元。默认激活的纹理单元是 GL_TEXTURE0,这里激活了 GL_TEXTURE1。
glBindTexture(GL_TEXTURE_2D, frame.textureId); // 绑定这个纹理到当前激活的纹理单元 GL_TEXTURE1。
glUniform1i(_mTextureUniform, 1); // 设置 _mTextureUniform 的对应的纹理单元为 1,即 GL_TEXTURE1,从而保证每个 uniform 采样器对应着正确的纹理单元。
if (_mPostionMatrixUniform >= 0) {
glUniformMatrix4fv(_mPostionMatrixUniform, 1, false, frame.mvpMatrix.m); // 把矩阵数据发送给着色器对应的参数。
}
// 启用顶点位置属性通道。
glEnableVertexAttribArray(_mPositionAttribute);
// 启用纹理坐标属性通道。
glEnableVertexAttribArray(_mTextureCoordinateAttribute);
static const GLfloat squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
// 关联顶点位置数据。
glVertexAttribPointer(_mPositionAttribute, 2, GL_FLOAT, 0, 0, squareVertices);
static GLfloat textureCoordinates[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
// 关联纹理坐标数据。
glVertexAttribPointer(_mTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
// 绘制前回调。回调中可以更新绘制需要的相关数据。
if (self.preDrawCallBack) {
self.preDrawCallBack();
}
// 绘制所有图元。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 绘制后回调。
if (self.postDrawCallBack) {
self.postDrawCallBack();
}
// 解绑纹理。
glBindTexture(GL_TEXTURE_2D, 0);
// 关闭顶点位置属性通道。
glDisableVertexAttribArray(_mPositionAttribute);
// 关闭纹理坐标属性通道。
glDisableVertexAttribArray(_mTextureCoordinateAttribute);
}
if (_mFrameBuffer != nil) {
// 解绑内部 FBO。
[_mFrameBuffer unbind];
}
if (_mFrameBuffer != nil) {
// 清理内部 FBO。
resultFrame.textureId = _mFrameBuffer.getTextureId;
resultFrame.textureSize = _mFrameBuffer.getSize;
}
// 返回渲染好的纹理。
return resultFrame;
}
@end
从上面的实现代码中可以看到,KFGLFilter 的核心接口是 - (KFTextureFrame *)render:(KFTextureFrame *)frame,这个接口接受输入一帧纹理,进行渲染处理后再输出一帧纹理,这也是 KFGLFilter 的核心功能。这样一来,KFGLFilter 作为一个渲染处理节点,可以支持多个节点串起来做更复杂的处理。
KFGLFilter 提供的获取内部的 FBO、获取 GL 程序、设置 GL 程序变量值、渲染一帧纹理、渲染前回调、渲染后回调等接口则可以支持该渲染节点与外部的数据交互。
3)OpenGL 模块:KFGLProgram、KFGLFrameBuffer、KFTextureFrame、KFGLTextureAttributes
KFGLFilter 中我们还使用了 KFGLProgram、KFGLFrameBuffer、KFTextureFrame、KFGLTextureAttributes 以及一些基础定义类,他们都是对 OpenGL API 的一些封装:
- KFGLProgram:封装了使用 GL 程序的部分 API。
- KFGLFrameBuffer:封装了使用 FBO 的 API。
- KFTextureFrame:表示一帧纹理对象。
- KFFrame:表示一帧,类型可以是数据缓冲或纹理。
- KFGLTextureAttributes:对纹理 Texture 属性的封装。
- KFGLBase:定义了默认的 VertexShader 和 FragmentShader。
- KFPixelBufferConvertTexture:将 CVPixelBuffer 转换为纹理 Texture 的工具类,兼容颜色空间的转换处理。
对应代码如下:
KFGLProgram.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// KFGLProgram 封装了使用 GL 程序的部分 API
@interface KFGLProgram : NSObject
- (instancetype)initWithVertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader;
- (void)use; // 使用 GL 程序
- (int)getUniformLocation:(NSString *)name; // 根据名字获取 uniform 位置值
- (int)getAttribLocation:(NSString *)name; // 根据名字获取 attribute 位置值
@end
NS_ASSUME_NONNULL_END
KFGLProgram.mm
#import "KFGLProgram.h"
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
@interface KFGLProgram () {
int _mProgram;
int _mVertexShader;
int _mFragmentShader;
}
@end
@implementation KFGLProgram
- (instancetype)initWithVertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader {
self = [super init];
if (self) {
[self _createProgram:vertexShader fragmentSource:fragmentShader];
}
return self;
}
- (void)dealloc {
if (_mVertexShader != 0) {
glDeleteShader(_mVertexShader);
_mVertexShader = 0;
}
if (_mFragmentShader != 0) {
glDeleteShader(_mFragmentShader);
_mFragmentShader = 0;
}
if (_mProgram != 0) {
glDeleteProgram(_mProgram);
_mProgram = 0;
}
}
// 使用 GL 程序。
- (void)use {
if (_mProgram != 0) {
glUseProgram(_mProgram);
}
}
// 根据名字获取 uniform 位置值
- (int)getUniformLocation:(NSString *)name {
return glGetUniformLocation(_mProgram, [name UTF8String]);
}
// 根据名字获取 attribute 位置值
- (int)getAttribLocation:(NSString *)name {
return glGetAttribLocation(_mProgram, [name UTF8String]);
}
// 加载和编译 shader,并链接 GL 程序。
- (void)_createProgram:(NSString *)vertexSource fragmentSource:(NSString *)fragmentSource {
_mVertexShader = [self _loadShader:GL_VERTEX_SHADER source:vertexSource];
_mFragmentShader = [self _loadShader:GL_FRAGMENT_SHADER source:fragmentSource];
if (_mVertexShader != 0 && _mFragmentShader != 0) {
_mProgram = glCreateProgram();
glAttachShader(_mProgram, _mVertexShader);
glAttachShader(_mProgram, _mFragmentShader);
glLinkProgram(_mProgram);
GLint linkStatus;
glGetProgramiv(_mProgram, GL_LINK_STATUS, &linkStatus);
if (linkStatus != GL_TRUE) {
glDeleteProgram(_mProgram);
_mProgram = 0;
}
}
}
// 加载和编译 shader。
- (int)_loadShader:(int)shaderType source:(NSString *)source {
int shader = glCreateShader(shaderType);
const GLchar *cSource = (GLchar *) [source UTF8String];
glShaderSource(shader,1, &cSource,NULL);
glCompileShader(shader);
GLint compiled;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
glDeleteShader(shader);
shader = 0;
}
return shader;
}
@end
本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
KFGLFrameBuffer.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "KFGLTextureAttributes.h"
NS_ASSUME_NONNULL_BEGIN
// 封装了对 FBO 使用的 API
@interface KFGLFrameBuffer : NSObject
- (instancetype)initWithSize:(CGSize)size;
- (instancetype)initWithSize:(CGSize)size textureAttributes:(KFGLTextureAttributes *)textureAttributes;
- (CGSize)getSize; // 纹理 size
- (GLuint)getTextureId; // 纹理 id
- (void)bind; // 绑定 FBO
- (void)unbind; // 解绑 FBO
@end
NS_ASSUME_NONNULL_END
KFGLFrameBuffer.mm
#import "KFGLFrameBuffer.h"
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
@interface KFGLFrameBuffer () {
GLuint _mTextureId;
GLuint _mFboId;
KFGLTextureAttributes *_mTextureAttributes;
CGSize _mSize;
int _mLastFboId;
}
@end
@implementation KFGLFrameBuffer
- (instancetype)initWithSize:(CGSize)size {
return [self initWithSize:size textureAttributes:[KFGLTextureAttributes new]];
}
- (instancetype)initWithSize:(CGSize)size textureAttributes:(KFGLTextureAttributes*)textureAttributes{
self = [super init];
if (self) {
_mTextureId = -1;
_mFboId = -1;
_mLastFboId = -1;
_mSize = size;
_mTextureAttributes = textureAttributes;
[self _setup];
}
return self;
}
- (void)dealloc {
if (_mTextureId != -1) {
glDeleteTextures(1, &_mTextureId);
_mTextureId = -1;
}
if (_mFboId != -1) {
glDeleteFramebuffers(1, &_mFboId);
_mFboId = -1;
}
}
- (CGSize)getSize {
return _mSize;
}
- (GLuint)getTextureId {
return _mTextureId;
}
- (void)bind {
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_mLastFboId);
if (_mFboId != -1) {
glBindFramebuffer(GL_FRAMEBUFFER, _mFboId);
glViewport(0, 0, _mSize.width, _mSize.height);
}
}
- (void)unbind {
glBindFramebuffer(GL_FRAMEBUFFER, _mLastFboId);
}
- (void)_setup {
[self _setupTexture];
[self _setupFrameBuffer];
[self _bindTexture2FrameBuffer];
}
-(void)_setupTexture {
if (_mTextureId == -1) {
glGenTextures(1, &_mTextureId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _mTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _mTextureAttributes.minFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _mTextureAttributes.magFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _mTextureAttributes.wrapS);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _mTextureAttributes.wrapT);
if ((int)_mSize.width % 4 != 0) {
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
}
glTexImage2D(GL_TEXTURE_2D, 0, _mTextureAttributes.internalFormat, _mSize.width, _mSize.height, 0, _mTextureAttributes.format, _mTextureAttributes.type, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
}
}
- (void)_setupFrameBuffer {
if (_mFboId == -1) {
glGenFramebuffers(1, &_mFboId);
}
}
- (void)_bindTexture2FrameBuffer {
if (_mFboId != -1 && _mTextureId != -1 && _mSize.width != 0 && _mSize.height != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, _mFboId);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _mTextureId, 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
NSAssert(status == GL_FRAMEBUFFER_COMPLETE, @"Incomplete filter FBO: %d", status);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
@end
KFTextureFrame.h
#import "KFFrame.h"
#import <UIKit/UIKit.h>
#import <CoreMedia/CoreMedia.h>
#import <GLKit/GLKit.h>
NS_ASSUME_NONNULL_BEGIN
// 表示一帧纹理对象
@interface KFTextureFrame : KFFrame
@property (nonatomic, assign) CGSize textureSize;
@property (nonatomic, assign) GLuint textureId;
@property (nonatomic, assign) CMTime time;
@property (nonatomic, assign) GLKMatrix4 mvpMatrix;
- (instancetype)initWithTextureId:(GLuint)textureId textureSize:(CGSize)textureSize time:(CMTime)time;
@end
NS_ASSUME_NONNULL_END
KFTextureFrame.m
#import "KFTextureFrame.h"
@implementation KFTextureFrame
- (instancetype)initWithTextureId:(GLuint)textureId textureSize:(CGSize)textureSize time:(CMTime)time {
self = [super init];
if(self){
_textureId = textureId;
_textureSize = textureSize;
_time = time;
_mvpMatrix = GLKMatrix4Identity;
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
KFTextureFrame *copy = [[KFTextureFrame allocWithZone:zone] init];
copy.textureId = _textureId;
copy.textureSize = _textureSize;
copy.time = _time;
copy.mvpMatrix = _mvpMatrix;
return copy;
}
@end
KFFrame.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KFFrameType) {
KFFrameBuffer = 0, // 数据缓冲区类型
KFFrameTexture = 1, // 纹理类型
};
@interface KFFrame : NSObject
@property (nonatomic, assign) KFFrameType frameType;
- (instancetype)initWithType:(KFFrameType)type;
@end
NS_ASSUME_NONNULL_END
KFFrame.m
#import "KFFrame.h"
@implementation KFFrame
- (instancetype)initWithType:(KFFrameType)type {
self = [super init];
if(self){
_frameType = type;
}
return self;
}
- (instancetype)init {
self = [super init];
if(self){
_frameType = KFFrameBuffer;
}
return self;
}
@end
KFGLTextureAttributes.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
// 对纹理 Texture 属性的封装
@interface KFGLTextureAttributes : NSObject
@property(nonatomic, assign) int minFilter; // GL_TEXTURE_MIN_FILTER,多个纹素对应一个片元时的处理方式
@property(nonatomic, assign) int magFilter; // GL_TEXTURE_MAG_FILTER,没有足够的纹素来映射片元时的处理方式
@property(nonatomic, assign) int wrapS; // GL_TEXTURE_WRAP_S,超出范围的纹理处理方式,ST 坐标 S
@property(nonatomic, assign) int wrapT; // GL_TEXTURE_WRAP_T,超出范围的纹理处理方式,ST 坐标 T
@property(nonatomic, assign) int internalFormat;
@property(nonatomic, assign) int format;
@property(nonatomic, assign) int type;
@end
NS_ASSUME_NONNULL_END
KFGLTextureAttributes.m
#import "KFGLTextureAttributes.h"
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
@implementation KFGLTextureAttributes
- (instancetype)init {
self = [super init];
if (self) {
_minFilter = GL_LINEAR; // 混合附近纹素的颜色来计算片元的颜色。
_magFilter = GL_LINEAR; // 混合附近纹素的颜色来计算片元的颜色。
_wrapS = GL_CLAMP_TO_EDGE; // 采样纹理边缘,即剩余部分显示纹理临近的边缘颜色值。
_wrapT = GL_CLAMP_TO_EDGE; // 采样纹理边缘,即剩余部分显示纹理临近的边缘颜色值。
_internalFormat = GL_RGBA;
_format = GL_RGBA;
_type = GL_UNSIGNED_BYTE;
}
return self;
}
@end
KFGLBase.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#define STRINGIZE(x) #x
#define STRINGIZE2(x) STRINGIZE(x)
#define SHADER_STRING(text) @ STRINGIZE2(text)
extern NSString *const KFDefaultVertexShader;
extern NSString *const KFDefaultFragmentShader;
NS_ASSUME_NONNULL_END
KFGLBase.m
#import "KFGLBase.h"
NSString *const KFDefaultVertexShader = SHADER_STRING
(
attribute vec4 position; // 通过 attribute 通道获取顶点信息。4 维向量。
attribute vec4 inputTextureCoordinate; // 通过 attribute 通道获取纹理坐标信息。4 维向量。
varying vec2 textureCoordinate; // 用于 vertex shader 和 fragment shader 间传递纹理坐标。2 维向量。
uniform mat4 mvpMatrix; // 通过 uniform 通道获取 mvp 矩阵信息。4x4 矩阵。
void main()
{
gl_Position = mvpMatrix * position; // 根据 mvp 矩阵和顶点信息计算渲染管线最终要用的顶点信息。
textureCoordinate = inputTextureCoordinate.xy; // 将通过 attribute 通道获取的纹理坐标数据中的 2 维分量传给 fragment shader。
}
);
NSString *const KFDefaultFragmentShader = SHADER_STRING
(
varying highp vec2 textureCoordinate; // 从 vertex shader 传递来的纹理坐标。
uniform sampler2D inputImageTexture; // 通过 uniform 通道获取纹理信息。2D 纹理。
void main()
{
gl_FragColor = texture2D(inputImageTexture, textureCoordinate); // texture2D 获取指定纹理在对应坐标位置的 rgba 颜色值,作为渲染管线最终要用的颜色信息。
}
);
KFPixelBufferConvertTexture.h
#import <Foundation/Foundation.h>
#import <OpenGLES/EAGL.h>
#import <CoreVideo/CoreVideo.h>
#import "KFTextureFrame.h"
NS_ASSUME_NONNULL_BEGIN
// KFPixelBufferConvertTexture 是一个将 CVPixelBuffer 转换为纹理 Texture 的工具类,兼容颜色空间的转换处理
@interface KFPixelBufferConvertTexture : NSObject
- (instancetype)initWithContext:(EAGLContext *)context;
- (KFTextureFrame *)renderFrame:(CVPixelBufferRef)pixelBuffer time:(CMTime)time; // 将 CVPixelBuffer 转换为纹理 Texture
@end
NS_ASSUME_NONNULL_END
KFPixelBufferConvertTexture.mm
#import "KFPixelBufferConvertTexture.h"
#import <OpenGLES/gltypes.h>
#import "KFGLFilter.h"
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFGLBase.h"
static const GLfloat kFTColorConversion601VideoRange[] = {
1.164, 1.164, 1.164,
0.0, -0.392, 2.017,
1.596, -0.813, 0.0,
};
static const GLfloat kFTColorConversion601FullRange[] = {
1.0, 1.0, 1.0,
0.0, -0.343, 1.765,
1.4, -0.711, 0.0,
};
static const GLfloat kFTColorConversion709VideoRange[] = {
1.164, 1.164, 1.164,
0.0, -0.213, 2.112,
1.793, -0.533, 0.0,
};
static const GLfloat kFTColorConversion709FullRange[] = {
1.0, 1.0, 1.0,
0.0, -0.187, 1.856,
1.575, -0.468, 0.0,
};
NSString *const kFYUV2RGBShader = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform sampler2D chrominanceTexture;
uniform mediump mat3 colorConversionMatrix;
uniform mediump int isFullRange;
void main()
{
mediump vec3 yuv;
lowp vec3 rgb;
if (isFullRange == 1) {
yuv.x = texture2D(inputImageTexture, textureCoordinate).r;
} else {
yuv.x = texture2D(inputImageTexture, textureCoordinate).r -(16.0 / 255.0);
}
yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
rgb = colorConversionMatrix * yuv;
gl_FragColor = vec4(rgb, 1);
}
);
@interface KFPixelBufferConvertTexture () {
KFGLFilter *_filter;
GLuint _chrominanceTexture;
BOOL _isFullRange;
const GLfloat *_yuvColorMatrix;
CVOpenGLESTextureCacheRef _textureCache;
}
@end
@implementation KFPixelBufferConvertTexture
- (instancetype)initWithContext:(EAGLContext *)context {
self = [super init];
if (self) {
CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, context, NULL, &_textureCache);
}
return self;
}
- (void)dealloc {
if (_textureCache) {
CVOpenGLESTextureCacheFlush(_textureCache, 0);
CFRelease(_textureCache);
_textureCache = NULL;
}
_filter = nil;
}
- (KFTextureFrame *)renderFrame:(CVPixelBufferRef)pixelBuffer time:(CMTime)time {
if (!pixelBuffer) {
return nil;
}
if (CVPixelBufferGetPlaneCount(pixelBuffer) > 0) {
return [self _yuvRenderFrame:pixelBuffer time:time];
}
return nil;
}
- (void)_setupYUVProgramMatrix:(BOOL)isFullRange colorSpace:(CFTypeRef)colorSpace {
if (colorSpace == kCVImageBufferYCbCrMatrix_ITU_R_601_4) {
_yuvColorMatrix = isFullRange ? kFTColorConversion601FullRange : kFTColorConversion601VideoRange;
} else {
_yuvColorMatrix = isFullRange ? kFTColorConversion709FullRange : kFTColorConversion709VideoRange;
}
_isFullRange = isFullRange;
if (!_filter) {
_filter = [[KFGLFilter alloc] initWithCustomFBO:NO vertexShader:KFDefaultVertexShader fragmentShader:kFYUV2RGBShader];
__weak typeof(self) _self = self;
_filter.preDrawCallBack = ^() {
__strong typeof(_self) sself = _self;
if (!sself) {
return;
}
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, sself->_chrominanceTexture);
glUniform1i([sself->_filter.getProgram getUniformLocation:@"chrominanceTexture"], 5);
glUniformMatrix3fv([sself->_filter.getProgram getUniformLocation:@"colorConversionMatrix"], 1, GL_FALSE, sself->_yuvColorMatrix);
glUniform1i([sself->_filter.getProgram getUniformLocation:@"isFullRange"], sself->_isFullRange ? 1 : 0);
};
}
}
- (BOOL)_pixelBufferIsFullRange:(CVPixelBufferRef)pixelBuffer {
// 判断 YUV 数据是否为 full range。
if (@available(iOS 15, *)) {
CFDictionaryRef cfDicAttributes = CVPixelBufferCopyCreationAttributes(pixelBuffer);
NSDictionary *dicAttributes = (__bridge_transfer NSDictionary*)cfDicAttributes;
if (dicAttributes && [dicAttributes objectForKey:@"PixelFormatDescription"]) {
NSDictionary *pixelFormatDescription = [dicAttributes objectForKey:@"PixelFormatDescription"];
if (pixelFormatDescription && [pixelFormatDescription objectForKey:(__bridge NSString*)kCVPixelFormatComponentRange]) {
NSString *componentRange = [pixelFormatDescription objectForKey:(__bridge NSString *)kCVPixelFormatComponentRange];
return [componentRange isEqualToString:(__bridge NSString *)kCVPixelFormatComponentRange_FullRange];
}
}
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
OSType formatType = CVPixelBufferGetPixelFormatType(pixelBuffer);
return formatType == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
#pragma clang diagnostic pop
}
return NO;
}
- (KFTextureFrame *)_yuvRenderFrame:(CVPixelBufferRef)pixelBuffer time:(CMTime)time{
BOOL isFullYUVRange = [self _pixelBufferIsFullRange:pixelBuffer];
CFTypeRef matrixKey = kCVImageBufferYCbCrMatrix_ITU_R_601_4;
if (@available(iOS 15, *)) {
matrixKey = CVBufferCopyAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
}else{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
matrixKey = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
#pragma clang diagnostic pop
}
[self _setupYUVProgramMatrix:isFullYUVRange colorSpace:matrixKey];
CVOpenGLESTextureRef luminanceTextureRef = NULL;
CVOpenGLESTextureRef chrominanceTextureRef = NULL;
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
CVReturn err;
glActiveTexture(GL_TEXTURE4);
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_LUMINANCE, (GLsizei)width, (GLsizei)height, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &luminanceTextureRef);
if (err){
NSLog(@"KFPixelBufferConvertTexture CVOpenGLESTextureCacheCreateTextureFromImage error");
return nil;
}
GLuint luminanceTexture = CVOpenGLESTextureGetName(luminanceTextureRef);
glBindTexture(GL_TEXTURE_2D, luminanceTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// UV-plane
glActiveTexture(GL_TEXTURE5);
size_t width_uv = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
size_t height_uv = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, (GLsizei)width_uv, (GLsizei)height_uv, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &chrominanceTextureRef);
if (err){
NSLog(@"KFPixelBufferConvertTexture CVOpenGLESTextureCacheCreateTextureFromImage error");
return nil;
}
_chrominanceTexture = CVOpenGLESTextureGetName(chrominanceTextureRef);
glBindTexture(GL_TEXTURE_2D, _chrominanceTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
KFTextureFrame *inputFrame = [[KFTextureFrame alloc] initWithTextureId:luminanceTexture textureSize:CGSizeMake(width, height) time:time];
KFTextureFrame *resultFrame = [_filter render:inputFrame];
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
if(luminanceTextureRef) CFRelease(luminanceTextureRef);
if(chrominanceTextureRef) CFRelease(chrominanceTextureRef);
return resultFrame;
}
@end
1.3、串联采集和渲染
最后就是在 ViewController 里将采集模块和渲染模块串起来了。代码如下:
KFVideoRenderViewController.m
#import "KFVideoRenderViewController.h"
#import "KFVideoCapture.h"
#import "KFPixelBufferConvertTexture.h"
#import "KFOpenGLView.h"
@interface KFVideoRenderViewController ()
@property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig;
@property (nonatomic, strong) KFVideoCapture *videoCapture;
@property (nonatomic, strong) KFOpenGLView *glView;
@property (nonatomic, strong) KFPixelBufferConvertTexture *pixelBufferConvertTexture;
@property (nonatomic, strong) EAGLContext *context;
@end
@implementation KFVideoRenderViewController
#pragma mark - Property
- (KFVideoCaptureConfig *)videoCaptureConfig {
if (!_videoCaptureConfig) {
_videoCaptureConfig = [[KFVideoCaptureConfig alloc] init];
}
return _videoCaptureConfig;
}
- (EAGLContext *)context {
if (!_context) {
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
}
return _context;
}
- (KFPixelBufferConvertTexture *)pixelBufferConvertTexture {
if (!_pixelBufferConvertTexture) {
_pixelBufferConvertTexture = [[KFPixelBufferConvertTexture alloc] initWithContext:self.context];
}
return _pixelBufferConvertTexture;
}
- (KFVideoCapture *)videoCapture {
if (!_videoCapture) {
_videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig];
__weak typeof(self) weakSelf = self;
_videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
// 视频采集数据回调。将采集回来的数据给渲染模块渲染。
[EAGLContext setCurrentContext:weakSelf.context];
KFTextureFrame *textureFrame = [weakSelf.pixelBufferConvertTexture renderFrame:CMSampleBufferGetImageBuffer(sampleBuffer) time:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
[weakSelf.glView displayFrame:textureFrame];
[EAGLContext setCurrentContext:nil];
};
_videoCapture.sessionErrorCallBack = ^(NSError* error) {
NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription);
};
}
return _videoCapture;
}
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self requestAccessForVideo];
[self setupUI];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.glView.frame = self.view.bounds;
}
#pragma mark - Action
- (void)changeCamera {
[self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack];
}
#pragma mark - Private Method
- (void)requestAccessForVideo {
__weak typeof(self) weakSelf = self;
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
switch (status) {
case AVAuthorizationStatusNotDetermined:{
// 许可对话没有出现,发起授权许可。
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
[weakSelf.videoCapture startRunning];
} else {
// 用户拒绝。
}
}];
break;
}
case AVAuthorizationStatusAuthorized:{
// 已经开启授权,可继续。
[weakSelf.videoCapture startRunning];
break;
}
default:
break;
}
}
- (void)setupUI {
self.edgesForExtendedLayout = UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars = YES;
self.title = @"Video Render";
self.view.backgroundColor = [UIColor whiteColor];
// Navigation item.
UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Camera" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)];
self.navigationItem.rightBarButtonItems = @[cameraBarButton];
// 渲染 view。
_glView = [[KFOpenGLView alloc] initWithFrame:self.view.bounds context:self.context];
_glView.fillMode = KFGLViewContentModeFill;
[self.view addSubview:self.glView];
}
@end
2、Android Demo
2.1、视频采集模块
1)配置类
定义一个 KFVideoCaptureConfig 用来配置视频采集参数。实际应用的采集分辨率与相机硬件有关,一般会根据配置的分辨率查找对应最合适的分辨率。
public class KFVideoCaptureConfig {
///< 摄像头方向
public Integer cameraFacing = CameraCharacteristics.LENS_FACING_FRONT;
///< 分辨率
public Size resolution = new Size(1080, 1920);
///< 帧率
public Integer fps = 30;
}
2)视频采集接口和实现
视频采集接口包含以下方法:
- 初始化
- 开始采集
- 停止采集
- 切换摄像头
- 释放实例
- 采集状态查询
- 获取 gl 上下文
public interface KFIVideoCapture {
///< 视频采集初始化
public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
///< 释放采集实例
public void release();
///< 开始采集
public void startRunning();
///< 关闭采集
public void stopRunning();
///< 是否正在采集
public boolean isRunning();
///< 获取 OpenGL 上下文
public EGLContext getEGLContext();
///< 切换摄像头
public void switchCamera();
}
安卓提供了两套应用级相机框架:camera1、camera2。两者区别如下:
- camera1,最初的 camera 框架,通过 android.hardware.Camera 类提供功能接口。无安卓版本限制。
- camera2,Android 5.0 引入的 api,通过 android.hardware.camera2 包提供功能接口。更新 camera2 的原因是 camera1 过于简单,没法满足更加复杂的相机应用场景,为了提供应用层更多控制相机的权限,才推出 camera2。安卓版本限制:requireApi >= 21。
2.1)KFVideoCaptureV1
KFVideoCaptureV1:使用 camera1 的 Demo 采集实现类。实现接口 KFIVideoCapture。
包括以下模块:
- 初始化:setup。读取相机配置;创建采集线程,在采集线程发送相机指令;创建渲染线程和 GLContext,渲染线程刷新纹理。
- 开始采集:startRunning。初始化相机实例,配置采集参数;设置 SurfaceTexture 给 Camera: mCamera.setPreviewTexture(mSurfaceTexture.getSurfaceTexture());开启相机预览。
- 回调采集数据:mSurfaceTextureListener。SurfaceTexture 接受 camera 采集纹理回调,在渲染线程拼装纹理数据返回给外层。
public class KFVideoCaptureV1 implements KFIVideoCapture {
public static final int KFVideoCaptureV1CameraDisableError = -3000;
private static final String TAG = "KFVideoCaptureV1";
private KFVideoCaptureListener mListener = null; ///< 回调
private KFVideoCaptureConfig mConfig = null; ///< 配置
private WeakReference<Context> mContext = null;
private boolean mCameraIsRunning = false; ///< 是否正在采集
private HandlerThread mCameraThread = null; ///< 采集线程
private Handler mCameraHandler = null;
private KFGLContext mGLContext = null; ///< GL 特效上下文
private KFSurfaceTexture mSurfaceTexture = null; ///< Surface 纹理
private KFGLFilter mOESConvert2DFilter; ///< 特效
private HandlerThread mRenderThread = null; ///< 渲染线程
private Handler mRenderHandler = null;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程
private Camera.CameraInfo mFrontCameraInfo = null; ///< 前置摄像头信息
private int mFrontCameraId = -1;
private Camera.CameraInfo mBackCameraInfo = null; ///< 后置摄像头信息
private int mBackCameraId = -1;
private Camera mCamera = null; ///< 当前摄像头实例(前置或者后置)
public KFVideoCaptureV1() {
}
@Override
public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext) {
mListener = listener;
mConfig = config;
mContext = new WeakReference<Context>(context);
///< 采集线程
mCameraThread = new HandlerThread("KFCameraThread");
mCameraThread.start();
mCameraHandler = new Handler((mCameraThread.getLooper()));
///< 渲染线程
mRenderThread = new HandlerThread("KFCameraRenderThread");
mRenderThread.start();
mRenderHandler = new Handler((mRenderThread.getLooper()));
///< OpenGL 上下文
mGLContext = new KFGLContext(eglShareContext);
}
@Override
public EGLContext getEGLContext() {
return mGLContext.getContext();
}
@Override
public boolean isRunning() {
return mCameraIsRunning;
}
@Override
public void release() {
mCameraHandler.post(() -> {
///< 停止视频采集 清晰视频采集实例、OpenGL 上下文、线程等
_stopRunning();
mGLContext.bind();
if(mSurfaceTexture != null){
mSurfaceTexture.release();
mSurfaceTexture = null;
}
if(mOESConvert2DFilter != null){
mOESConvert2DFilter.release();
mOESConvert2DFilter = null;
}
mGLContext.unbind();
mGLContext.release();
mGLContext = null;
if(mCamera != null){
mCamera.release();
mCamera = null;
}
mCameraThread.quit();
mRenderThread.quit();
});
}
@Override
public void startRunning() {
mCameraHandler.post(() -> {
///< 检测视频采集权限
if (ActivityCompat.checkSelfPermission(mContext.get(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) mContext.get(), new String[] {Manifest.permission.CAMERA}, 1);
}
///< 检测相机是否可用
if(!_checkCameraService()){
_callBackError(KFVideoCaptureV1CameraDisableError,"相机不可用");
return;
}
///< 开启视频采集
_startRunning();
});
}
@Override
public void stopRunning() {
mCameraHandler.post(() -> {
_stopRunning();
});
}
@Override
public void switchCamera() {
mCameraHandler.post(() -> {
///< 切换摄像头,先关闭相机调整方向再打开相机
_stopRunning();
mConfig.cameraFacing = mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT;
_startRunning();
});
}
private void _startRunning() {
///< 获取前后台摄像机信息
if(mFrontCameraInfo == null || mBackCameraInfo == null){
_initCameraInfo();
}
try {
///< 根据前后台摄像头 id 打开相机实例
mCamera = Camera.open(_getCurrentCameraId());
if(mCamera != null){
///< 设置相机各分辨率、帧率、方向
Camera.Parameters parameters = mCamera.getParameters();
Size previewSize = _getOptimalSize(mConfig.resolution.getWidth(), mConfig.resolution.getHeight());
mConfig.resolution = new Size(previewSize.getHeight(),previewSize.getWidth());
parameters.setPreviewSize(previewSize.getWidth(),previewSize.getHeight());
Range<Integer> selectFpsRange = _chooseFpsRange();
if(selectFpsRange.getUpper() > 0) {
parameters.setPreviewFpsRange(selectFpsRange.getLower(),selectFpsRange.getUpper());
}
mCamera.setParameters(parameters);
mCamera.setDisplayOrientation(_getDisplayOrientation());
///< 创建 Surface 纹理
if(mSurfaceTexture == null){
mGLContext.bind();
mSurfaceTexture = new KFSurfaceTexture(mSurfaceTextureListener);
mOESConvert2DFilter = new KFGLFilter(false, KFGLBase.defaultVertexShader,KFGLBase.oesFragmentShader);
mGLContext.unbind();
}
///< 设置 SurfaceTexture 给 Camera,这样 Camera 自动将数据渲染到 SurfaceTexture
mCamera.setPreviewTexture(mSurfaceTexture.getSurfaceTexture());
///< 开启预览
mCamera.startPreview();
mCameraIsRunning = true;
if(mListener != null){
mMainHandler.post(()->{
///< 回调相机打开
mListener.cameraOnOpened();
});
}
}
} catch (RuntimeException | IOException e) {
e.printStackTrace();
}
}
private void _stopRunning() {
if(mCamera != null){
///< 关闭相机采集
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
mCameraIsRunning = false;
if(mListener != null){
mMainHandler.post(()->{
///< 回调相机关闭
mListener.cameraOnClosed();
});
}
}
}
private int _getCurrentCameraId() {
///< 获取当前摄像机 id
if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
return mFrontCameraId;
} else if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_BACK) {
return mBackCameraId;
} else {
throw new RuntimeException("No available camera id found.");
}
}
private int _getDisplayOrientation() {
///< 获取摄像机需要旋转的方向
int orientation = 0;
if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
orientation = (_getCurrentCameraInfo().orientation) % 360;
orientation = (360 - orientation) % 360;
} else {
orientation = (_getCurrentCameraInfo().orientation + 360) % 360;
}
return orientation;
}
private Camera.CameraInfo _getCurrentCameraInfo() {
///< 获取当前摄像机描述信息
if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
return mFrontCameraInfo;
} else if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_BACK) {
return mBackCameraInfo;
} else {
throw new RuntimeException("No available camera id found.");
}
}
private Size _getOptimalSize(int width, int height) {
///< 根据外层输入分辨率查找对应最合适的分辨率
List<Camera.Size> sizeMap = mCamera.getParameters().getSupportedPreviewSizes();
List<Size> sizeList = new ArrayList<>();
for (Camera.Size option:sizeMap) {
if (width > height) {
if (option.width >= width && option.height >= height) {
sizeList.add(new Size(option.width,option.height));
}
} else {
if (option.width >= height && option.height >= width) {
sizeList.add(new Size(option.width,option.height));
}
}
}
if (sizeList.size() > 0) {
return Collections.min(sizeList, new Comparator<Size>() {
@Override
public int compare(Size o1, Size o2) {
return Long.signum(o1.getWidth() * o1.getHeight() - o2.getWidth() * o2.getHeight());
}
});
}
return new Size(0,0);
}
private Range<Integer> _chooseFpsRange() {
///< 根据外层设置帧率查找最合适的帧率
List<int[]> fpsRange = mCamera.getParameters().getSupportedPreviewFpsRange();
for(int[] range : fpsRange){
if(range.length == 2 && range[1] >= mConfig.fps*1000 && range[0] <= mConfig.fps*1000){
// return new Range<>(range[0],mConfig.fps*1000);
return new Range<>(range[0],range[1]); ///< 仅支持列表中一项,不能像 camera2 一样指定
}
}
return new Range<Integer>(0,0);
}
private void _initCameraInfo() {
///< 获取前置后置摄像头描述信息与 id
int numberOfCameras = Camera.getNumberOfCameras();
for (int cameraId = 0; cameraId < numberOfCameras; cameraId++) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
// 后置摄像头信息
mBackCameraId = cameraId;
mBackCameraInfo = cameraInfo;
} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
// 前置摄像头信息
mFrontCameraId = cameraId;
mFrontCameraInfo = cameraInfo;
}
}
}
private boolean _checkCameraService(){
///< 检测相机是否可用
DevicePolicyManager dpm = (DevicePolicyManager)mContext.get().getSystemService(Context.DEVICE_POLICY_SERVICE);
if (dpm.getCameraDisabled(null)) {
return false;
}
return true;
}
private void _callBackError(int error, String errorMsg){
///< 错误回调
if(mListener != null){
mMainHandler.post(()->{
mListener.cameraOnError(error,TAG + errorMsg);
});
}
}
private KFSurfaceTextureListener mSurfaceTextureListener = new KFSurfaceTextureListener() {
@Override
///< SurfaceTexture 数据回调
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
mRenderHandler.post(()->{
long timestamp = System.nanoTime();
mGLContext.bind();
///< 刷新纹理数据至 SurfaceTexture
mSurfaceTexture.getSurfaceTexture().updateTexImage();
if(mListener != null){
///< 拼装好纹理数据返回给外层
KFTextureFrame frame = new KFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),mConfig.resolution,timestamp,true);
mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
KFFrame convertFrame = mOESConvert2DFilter.render(frame);
mListener.onFrameAvailable(convertFrame);
}
mGLContext.unbind();
});
}
};
}
本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
3.2)KFVideoCaptureV1
KFVideoCaptureV2:使用 camera2 的 demo 采集实现类。
实现接口 KFIVideoCapture。模块设计和 KFVideoCaptureV1 基本一致,只是 camera api 调用有差异,不再赘述。
public class KFVideoCaptureV2 implements KFIVideoCapture {
public static final int KFVideoCaptureV2CameraDisableError = -3000;
private static final String TAG = "KFVideoCaptureV2";
private KFVideoCaptureListener mListener = null; ///< 回调
private KFVideoCaptureConfig mConfig = null; ///< 采集配置
private WeakReference<Context> mContext = null;
private CameraManager mCameraManager = null; ///< 相机系统服务,用于管理和连接相机设备
private String mCameraId; ///<摄像头id
private CameraDevice mCameraDevice = null; ///< 相机设备类
private HandlerThread mCameraThread = null; ///< 采集线程
private Handler mCameraHandler = null;
private CaptureRequest.Builder mCaptureRequestBuilder = null; ///< CaptureRequest 的构造器,使用 Builder 模式,设置更加方便
private CaptureRequest mCaptureRequest = null; ///< 相机捕获图像的设置请求,包含传感器、镜头、闪光灯等
private CameraCaptureSession mCameraCaptureSession = null; ///< 请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道,源端是相机,另一端是 Target
private boolean mCameraIsRunning = false;
private Range<Integer>[] mFpsRange;
private KFGLContext mGLContext = null;
private KFSurfaceTexture mSurfaceTexture = null;
private KFGLFilter mOESConvert2DFilter; ///< 特效
private Surface mSurface = null;
private HandlerThread mRenderThread = null;
private Handler mRenderHandler = null;
private Handler mMainHandler = new Handler(Looper.getMainLooper());
public KFVideoCaptureV2() {
}
@Override
public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext) {
mListener = listener;
mConfig = config;
mContext = new WeakReference<Context>(context);
///< 相机采集线程
mCameraThread = new HandlerThread("KFCameraThread");
mCameraThread.start();
mCameraHandler = new Handler((mCameraThread.getLooper()));
///< 渲染线程
mRenderThread = new HandlerThread("KFCameraRenderThread");
mRenderThread.start();
mRenderHandler = new Handler((mRenderThread.getLooper()));
mGLContext = new KFGLContext(eglShareContext);
}
@Override
public EGLContext getEGLContext() {
return mGLContext.getContext();
}
@Override
public boolean isRunning() {
return mCameraIsRunning;
}
@Override
public void startRunning() {
///< 开启预览
mCameraHandler.post(() -> {
_startRunning();
});
}
@Override
public void stopRunning() {
///< 停止预览
mCameraHandler.post(() -> {
_stopRunning();
});
}
@Override
public void release() {
mCameraHandler.post(() -> {
///< 关闭采集、释放 SurfaceTexture、OpenGL 上下文、线程等
_stopRunning();
mGLContext.bind();
if(mSurfaceTexture != null){
mSurfaceTexture.release();
mSurfaceTexture = null;
}
if(mOESConvert2DFilter != null){
mOESConvert2DFilter.release();
mOESConvert2DFilter = null;
}
mGLContext.unbind();
mGLContext.release();
mGLContext = null;
if(mSurface != null){
mSurface.release();
mSurface = null;
}
mCameraThread.quit();
mRenderThread.quit();
});
}
@Override
public void switchCamera() {
///< 切换摄像头
mCameraHandler.post(() -> {
_stopRunning();
mConfig.cameraFacing = mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT;
_startRunning();
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _startRunning() {
///< 获取相机系统服务
if(mCameraManager == null){
mCameraManager = (CameraManager) mContext.get().getSystemService(Context.CAMERA_SERVICE);
}
///< 根据外层摄像头方向查找摄像头 id
boolean selectSuccess = _chooseCamera();
if (selectSuccess) {
try {
///< 检测采集权限
if (ActivityCompat.checkSelfPermission(mContext.get(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) mContext.get(), new String[] {Manifest.permission.CAMERA}, 1);
}
///< 检测相机是否可用
if(!_checkCameraService()){
_callBackError(KFVideoCaptureV2CameraDisableError, "相机不可用");
return;
}
///< 打开相机设备
mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}
private void _stopRunning() {
///< 停止采集
if(mCameraCaptureSession != null) {
mCameraCaptureSession.close();
mCameraCaptureSession = null;
}
if(mCameraDevice != null){
mCameraDevice.close();
mCameraDevice = null;
}
}
private KFSurfaceTextureListener mSurfaceTextureListener = new KFSurfaceTextureListener() {
@Override
//< SurfaceTexture 数据回调
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
mRenderHandler.post(() -> {
long timestamp = System.nanoTime();
mGLContext.bind();
///< 刷新纹理数据至 SurfaceTexture
mSurfaceTexture.getSurfaceTexture().updateTexImage();
if(mListener != null){
///< 拼装好纹理数据返回给外层
KFTextureFrame frame = new KFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),mConfig.resolution,timestamp,true);
mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
KFFrame convertFrame = mOESConvert2DFilter.render(frame);
mListener.onFrameAvailable(convertFrame);
}
mGLContext.unbind();
});
}
};
private CameraCaptureSession.StateCallback mCaputreSessionCallback = new CameraCaptureSession.StateCallback() {
@Override
///< 创建会话回调
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
///< 创建CaptureRequest
mCaptureRequest = mCaptureRequestBuilder.build();
mCameraCaptureSession = cameraCaptureSession;
try {
///< 通过连续重复的 Capture 实现预览功能,每次 Capture 会把预览画面显示到对应的 Surface 上
mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
///< 创建会话出错回调
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
_callBackError(1005,"onConfigureFailed");
}
};
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
///< 相机打开回调
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
try {
///< 通过相机设备创建构造器
mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
Range<Integer> selectFpsRange = _chooseFpsRange();
///< 设置帧率
if(selectFpsRange.getUpper() > 0){
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,selectFpsRange);
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
if(mListener != null){
mMainHandler.post(()->{
mListener.cameraOnOpened();
});
}
mCameraIsRunning = true;
if(mSurfaceTexture == null){
mGLContext.bind();
mSurfaceTexture = new KFSurfaceTexture(mSurfaceTextureListener);
mOESConvert2DFilter = new KFGLFilter(false, KFGLBase.defaultVertexShader,KFGLBase.oesFragmentShader);
mGLContext.unbind();
mSurface = new Surface(mSurfaceTexture.getSurfaceTexture());
}
if(mSurface != null) {
///< 设置目标输出 Surface
mSurfaceTexture.getSurfaceTexture().setDefaultBufferSize(mConfig.resolution.getHeight(),mConfig.resolution.getWidth());
mCaptureRequestBuilder.addTarget(mSurface);
try {
///< 创建通道会话
mCameraDevice.createCaptureSession(Arrays.asList(mSurface), mCaputreSessionCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
///< 相机断开连接回调
camera.close();
mCameraDevice = null;
mCameraIsRunning = false;
}
@Override
public void onClosed(@NonNull CameraDevice camera) {
///< 相机关闭回调
camera.close();
mCameraDevice = null;
if(mListener != null){
mMainHandler.post(()->{
mListener.cameraOnClosed();
});
}
mCameraIsRunning = false;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
///< 相机出错回调
camera.close();
mCameraDevice = null;
_callBackError(error,"Camera onError");
mCameraIsRunning = false;
}
};
private boolean _chooseCamera() {
try {
///< 根据外层配置方向选择合适的设备 id 与 FPS 区间
final String[] ids = mCameraManager.getCameraIdList();
for(String cameraId : ids) {
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if(facing == mConfig.cameraFacing){
mCameraId = cameraId;
mFpsRange = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map != null) {
Size previewSize = _getOptimalSize(map.getOutputSizes(SurfaceTexture.class), mConfig.resolution.getWidth(), mConfig.resolution.getHeight());
// Range<Integer>[] fpsRanges = map.getHighSpeedVideoFpsRangesFor(previewSize); ///< high fps range
mConfig.resolution = new Size(previewSize.getHeight(),previewSize.getWidth());
}
return true;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
return false;
}
private Size _getOptimalSize(Size[] sizeMap, int width, int height) {
///< 根据外层配置分辨率寻找合适的分辨率
List<Size> sizeList = new ArrayList<>();
for (Size option : sizeMap) {
if (width > height) {
if (option.getWidth() >= width && option.getHeight() >= height) {
sizeList.add(option);
}
} else {
if (option.getWidth() >= height && option.getHeight() >= width) {
sizeList.add(option);
}
}
}
if (sizeList.size() > 0) {
return Collections.min(sizeList, new Comparator<Size>() {
@Override
public int compare(Size o1, Size o2) {
return Long.signum(o1.getWidth() * o1.getHeight() - o2.getWidth() * o2.getHeight());
}
});
}
return sizeMap[0];
}
private boolean _checkCameraService(){
///< 检测相机是否可用
DevicePolicyManager dpm = (DevicePolicyManager)mContext.get().getSystemService(Context.DEVICE_POLICY_SERVICE);
if (dpm.getCameraDisabled(null)) {
return false;
}
return true;
}
private void _callBackError(int error, String errorMsg){
///< 错误回调
if(mListener != null){
mMainHandler.post(()->{
mListener.cameraOnError(error,TAG + errorMsg);
});
}
}
private Range<Integer> _chooseFpsRange() {
///< 根据外层配置的帧率寻找合适的帧率
for(Range<Integer> range : mFpsRange){
if(range.getUpper() >= mConfig.fps && range.getLower() <= mConfig.fps){
return new Range<>(range.getLower(),mConfig.fps);
}
}
return new Range<Integer>(0,0);
}
}
2.2、视频渲染模块
1)KFGLContext
负责创建 OpenGL 环境,实现与 RenderDemo(1):用 OpenGL 画一个三角形 中一样,此处不再重复介绍。
2)KFGLFilter
KFGLFilter 是一个自定义滤镜,外部输入纹理,进行自定义效果渲染。
绘制流程和绘制三角形一致:加载编译 shader,链接到 shader 程序,设置顶点数据,绘制三角形。
Demo 中的 shader 只是最简单的纹理绘制,可以修改 shader 实现相机滤镜、美颜等效果。
public class KFGLFilter {
private boolean mIsCustomFBO = false; // 是否自定义帧缓存 部分渲染到指定 Surface 等其它场景会自定义
private KFGLFrameBuffer mFrameBuffer = null; // 帧缓存
private KFGLProgram mProgram = null; // 着色器容器
private KFGLTextureAttributes mGLTextureAttributes = null; // 纹理格式描述
private int mTextureUniform = -1; // 纹理下标
private int mPostionMatrixUniform = -1; // 顶点矩阵下标
private int mTextureMatrixUniform = -1; // 纹理矩阵下标
private int mPositionAttribute = -1; // 顶点下标
private int mTextureCoordinateAttribute = -1; // 纹理下标
private FloatBuffer mSquareVerticesBuffer = null; // 顶点 buffer
private FloatBuffer mTextureCoordinatesBuffer = null; // 纹理 buffer
private FloatBuffer mCustomSquareVerticesBuffer = null; // 自定义顶点 buffer
private FloatBuffer mCustomTextureCoordinatesBuffer = null; // 自定义纹理 buffer
public KFGLFilter(boolean isCustomFBO,String vertexShader,String fragmentShader) {
mIsCustomFBO = isCustomFBO;
// 初始化着色器
_setupProgram(vertexShader,fragmentShader);
}
public KFGLFilter(boolean isCustomFBO,String vertexShader,String fragmentShader, KFGLTextureAttributes textureAttributes) {
mIsCustomFBO = isCustomFBO;
mGLTextureAttributes = textureAttributes;
// 初始化着色器
_setupProgram(vertexShader,fragmentShader);
}
public KFGLFrameBuffer getOutputFrameBuffer() {
return mFrameBuffer;
}
public KFFrame render(KFTextureFrame frame){
if(frame == null){
return frame;
}
KFTextureFrame resultFrame = new KFTextureFrame(frame);
// 初始化帧缓存
_setupFrameBuffer(frame.textureSize);
// 绑定帧缓存
if(mFrameBuffer != null){
mFrameBuffer.bind();
}
if(mProgram != null){
// 使用着色器
mProgram.use();
// 设置帧缓存背景色
glClearColor(0,0,0,1);
// 清空帧缓存颜色
glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 激活纹理单元 1
glActiveTexture(GLES20.GL_TEXTURE1);
// 根据是否 OES 纹理绑定纹理 id
if (frame.isOESTexture) {
glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, frame.textureId);
} else {
glBindTexture(GLES20.GL_TEXTURE_2D, frame.textureId);
}
// 传递纹理单元 1
glUniform1i(mTextureUniform, 1);
// 设置纹理矩阵
if(mTextureMatrixUniform >= 0){
glUniformMatrix4fv(mTextureMatrixUniform, 1, false, frame.textureMatrix, 0);
}
// 设置顶点矩阵
if(mPostionMatrixUniform >= 0){
glUniformMatrix4fv(mPostionMatrixUniform, 1, false, frame.positionMatrix, 0);
}
// 启用顶点着色器顶点坐标属性
glEnableVertexAttribArray(mPositionAttribute);
// 启用顶点着色器纹理坐标属性
glEnableVertexAttribArray(mTextureCoordinateAttribute);
// 根据自定义顶点缓存设置不同顶点坐标
if(mCustomSquareVerticesBuffer != null){
mCustomSquareVerticesBuffer.position(0);
glVertexAttribPointer(mPositionAttribute, 2, GLES20.GL_FLOAT, false, 0, mCustomSquareVerticesBuffer);
}else{
mSquareVerticesBuffer.position(0);
glVertexAttribPointer(mPositionAttribute, 2, GLES20.GL_FLOAT, false, 0, mSquareVerticesBuffer);
}
// 根据自定义纹理缓存设置不同纹理坐标
if(mCustomTextureCoordinatesBuffer != null){
mCustomTextureCoordinatesBuffer.position(0);
glVertexAttribPointer(mTextureCoordinateAttribute, 2, GLES20.GL_FLOAT, false, 0, mCustomTextureCoordinatesBuffer);
}else{
mTextureCoordinatesBuffer.position(0);
glVertexAttribPointer(mTextureCoordinateAttribute, 2, GLES20.GL_FLOAT, false, 0, mTextureCoordinatesBuffer);
}
// 真正的渲染
glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
// 解除绑定纹理
if (frame.isOESTexture) {
glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
} else {
glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
// 关闭顶点着色器顶点属性
glDisableVertexAttribArray(mPositionAttribute);
// 关闭顶点着色器纹理属性
glDisableVertexAttribArray(mTextureCoordinateAttribute);
}
// 解绑帧缓存
if(mFrameBuffer != null){
mFrameBuffer.unbind();
}
// 返回渲染后数据
if(mFrameBuffer != null){
resultFrame.textureId = mFrameBuffer.getTextureId();
resultFrame.textureSize = mFrameBuffer.getSize();
resultFrame.isOESTexture = false;
resultFrame.textureMatrix = KFGLBase.KFIdentityMatrix();
resultFrame.positionMatrix = KFGLBase.KFIdentityMatrix();
}
return resultFrame;
}
public void release() {
// 释放帧缓存、着色器
if(mFrameBuffer != null){
mFrameBuffer.release();
mFrameBuffer = null;
}
if(mProgram != null){
mProgram.release();
mProgram = null;
}
}
public void setSquareVerticesBuffer(FloatBuffer squareVerticesBuffer) {
mSquareVerticesBuffer = squareVerticesBuffer;
}
public void setTextureCoordinatesBuffer(FloatBuffer textureCoordinatesBuffer) {
mCustomTextureCoordinatesBuffer = textureCoordinatesBuffer;
}
public void setIntegerUniformValue(String uniformName, int intValue){
// 设置 int 类型 uniform 数据
if(mProgram != null){
int uniforamIndex = mProgram.getUniformLocation(uniformName);
mProgram.use();
glUniform1i(uniforamIndex, intValue);
}
}
public void setFloatUniformValue(String uniformName, float floatValue){
// 设置 float 类型 uniform 数据
if(mProgram != null){
int uniforamIndex = mProgram.getUniformLocation(uniformName);
mProgram.use();
glUniform1f(uniforamIndex, floatValue);
}
}
private void _setupFrameBuffer(Size size) {
if(mIsCustomFBO) {
return;
}
// 初始化帧缓存与对应纹理
if(mFrameBuffer == null || mFrameBuffer.getSize().getWidth() != size.getWidth() || mFrameBuffer.getSize().getHeight() != size.getHeight()){
if(mFrameBuffer != null){
mFrameBuffer.release();
mFrameBuffer = null;
}
mFrameBuffer = new KFGLFrameBuffer(size,mGLTextureAttributes);
}
}
private void _setupProgram(String vertexShader,String fragmentShader){
// 根据 vs fs 初始化着色器容器
if(mProgram == null){
mProgram = new KFGLProgram(vertexShader,fragmentShader);
mTextureUniform = mProgram.getUniformLocation("inputImageTexture");
mPostionMatrixUniform = mProgram.getUniformLocation("mvpMatrix");
mTextureMatrixUniform = mProgram.getUniformLocation("textureMatrix");
mPositionAttribute = mProgram.getAttribLocation("position");
mTextureCoordinateAttribute = mProgram.getAttribLocation("inputTextureCoordinate");
final float squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * squareVertices.length);
squareVerticesByteBuffer.order(ByteOrder.nativeOrder());
mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer();
mSquareVerticesBuffer.put(squareVertices);
mSquareVerticesBuffer.position(0);
final float textureCoordinates[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
ByteBuffer textureCoordinatesByteBuffer = ByteBuffer.allocateDirect(4 * textureCoordinates.length);
textureCoordinatesByteBuffer.order(ByteOrder.nativeOrder());
mTextureCoordinatesBuffer = textureCoordinatesByteBuffer.asFloatBuffer();
mTextureCoordinatesBuffer.put(textureCoordinates);
mTextureCoordinatesBuffer.position(0);
}
}
}
3)KFRenderView
KFRenderView 是渲染模块,实现如下:
- 初始化渲染视图:可选 TextureView 或 SurfaceView 作为实际的渲染视图,添加视图到父布局。
- 渲染:在相机采集纹理的回调里,承接外部输入纹理给 KFGLFilter,渲染到 View 的 Surface 上。
- 销毁:释放 GL 上下文,释放渲染时的帧缓存、着色器。
public class KFRenderView extends ViewGroup {
private KFGLContext mEGLContext = null; // OpenGL上下文
private KFGLFilter mFilter = null; // 特效渲染到指定 Surface
private EGLContext mShareContext = null; // 共享上下文
private View mRenderView = null; // 渲染视图基类
private int mSurfaceWidth = 0; // 渲染缓存宽
private int mSurfaceHeight = 0; // 渲染缓存高
private FloatBuffer mSquareVerticesBuffer = null; // 自定义顶点
private KFRenderMode mRenderMode = KFRenderMode.KFRenderModeFill; // 自适应模式 黑边 比例填冲
private boolean mSurfaceChanged = false; // 渲染缓存是否变更
private Size mLastRenderSize = new Size(0,0); // 标记上次渲染 Size
public enum KFRenderMode {
KFRenderStretch,// 拉伸满-可能变形
KFRenderModeFit,// 黑边
KFRenderModeFill// 比例填充
};
public KFRenderView(Context context, EGLContext eglContext){
super(context);
mShareContext = eglContext; // 共享上下文
_setupSquareVertices(); // 初始化顶点
boolean isSurfaceView = false; // TextureView 与 SurfaceView 开关
if(isSurfaceView){
mRenderView = new KFSurfaceView(context, mListener);
}else{
mRenderView = new KFTextureView(context, mListener);
}
this.addView(mRenderView); // 添加视图到父视图
}
public void release() {
// 释放 GL 上下文、特效
if(mEGLContext != null){
mEGLContext.bind();
if(mFilter != null){
mFilter.release();
mFilter = null;
}
mEGLContext.unbind();
mEGLContext.release();
mEGLContext = null;
}
}
public void render(KFTextureFrame inputFrame){
if(inputFrame == null){
return;
}
//输入纹理使用自定义特效渲染到 View 的 Surface 上
if(mEGLContext != null && mFilter != null){
boolean frameResolutionChanged = inputFrame.textureSize.getWidth() != mLastRenderSize.getWidth() || inputFrame.textureSize.getHeight() != mLastRenderSize.getHeight();
// 渲染缓存变更或者视图大小变更重新设置顶点
if(mSurfaceChanged || frameResolutionChanged){
_recalculateVertices(inputFrame.textureSize);
mSurfaceChanged = false;
mLastRenderSize = inputFrame.textureSize;
}
// 渲染到指定 Surface
mEGLContext.bind();
mFilter.setSquareVerticesBuffer(mSquareVerticesBuffer);
GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight);
mFilter.render(inputFrame);
mEGLContext.swapBuffers();
mEGLContext.unbind();
}
}
private KFRenderListener mListener = new KFRenderListener() {
@Override
// 渲染缓存创建
public void surfaceCreate(@NonNull Surface surface) {
mEGLContext = new KFGLContext(mShareContext,surface);
// 初始化特效
mEGLContext.bind();
_setupFilter();
mEGLContext.unbind();
}
@Override
// 渲染缓存变更
public void surfaceChanged(@NonNull Surface surface, int width, int height) {
mSurfaceWidth = width;
mSurfaceHeight = height;
mSurfaceChanged = true;
// 设置 GL 上下文 Surface
mEGLContext.bind();
mEGLContext.setSurface(surface);
mEGLContext.unbind();
}
@Override
public void surfaceDestroy(@NonNull Surface surface) {
}
};
private void _setupFilter() {
// 初始化特效
if(mFilter == null){
mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader);
}
}
private void _setupSquareVertices() {
// 初始化顶点缓存
final float squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * squareVertices.length);
squareVerticesByteBuffer.order(ByteOrder.nativeOrder());
mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer();
mSquareVerticesBuffer.put(squareVertices);
mSquareVerticesBuffer.position(0);
}
private void _recalculateVertices(Size inputImageSize){
// 按照适应模式创建顶点
if(mSurfaceWidth == 0 || mSurfaceHeight == 0){
return;
}
Size renderSize = new Size(mSurfaceWidth,mSurfaceHeight);
float heightScaling = 1, widthScaling = 1;
Size insetSize = new Size(0,0);
float inputAspectRatio = (float) inputImageSize.getWidth() / (float)inputImageSize.getHeight();
float outputAspectRatio = (float)renderSize.getWidth() / (float)renderSize.getHeight();
boolean isAutomaticHeight = inputAspectRatio <= outputAspectRatio ? false : true;
if (isAutomaticHeight) {
float insetSizeHeight = (float)inputImageSize.getHeight() / ((float)inputImageSize.getWidth() / (float)renderSize.getWidth());
insetSize = new Size(renderSize.getWidth(),(int)insetSizeHeight);
} else {
float insetSizeWidth = (float)inputImageSize.getWidth() / ((float)inputImageSize.getHeight() / (float)renderSize.getHeight());
insetSize = new Size((int)insetSizeWidth,renderSize.getHeight());
}
switch (mRenderMode) {
case KFRenderStretch: {
widthScaling = 1;
heightScaling = 1;
}; break;
case KFRenderModeFit: {
widthScaling = (float)insetSize.getWidth() / (float)renderSize.getWidth();
heightScaling = (float)insetSize.getHeight() / (float)renderSize.getHeight();
}; break;
case KFRenderModeFill: {
widthScaling = (float) renderSize.getHeight() / (float)insetSize.getHeight();
heightScaling = (float)renderSize.getWidth() / (float)insetSize.getWidth();
}; break;
}
final float squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
final float customVertices[] = {
-widthScaling, -heightScaling,
widthScaling, -heightScaling,
-widthScaling, heightScaling,
widthScaling, heightScaling,
};
ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * customVertices.length);
squareVerticesByteBuffer.order(ByteOrder.nativeOrder());
mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer();
mSquareVerticesBuffer.put(customVertices);
mSquareVerticesBuffer.position(0);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 视图变更 Size
this.mRenderView.layout(left,top,right,bottom);
}
}
2.3、串联采集和渲染
MainActivity 中串联采集和渲染模块,实现相机图像实时预览功能。包括如下过程:
- 初始化采集事例:创建 KFIVideoCapture 实例并启动采集。
- 初始化渲染视图:创建 KFRenderView 并添加到 Demo 视图。
- 采集数据回调给渲染:KFIVideoCapture 注册监听 KFVideoCaptureListener,每帧采集触发回调 onFrameAvailable(frame),将回调数据输入 KFRenderView 驱动渲染,实现实时预览。
public class MainActivity extends AppCompatActivity {
private KFIVideoCapture mCapture;
private KFVideoCaptureConfig mCaptureConfig;
private KFRenderView mRenderView;
private KFGLContext mGLContext;
private Button cameraButton;
private Button playerButton;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
playerButton = findViewById(R.id.player_btn);
cameraButton = findViewById(R.id.camera_btn);
playerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
mGLContext = new KFGLContext(null);
mRenderView = new KFRenderView(this,mGLContext.getContext());
WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
Rect outRect = new Rect();
windowManager.getDefaultDisplay().getRectSize(outRect);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
addContentView(mRenderView,params);
mCaptureConfig = new KFVideoCaptureConfig();
mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
mCaptureConfig.resolution = new Size(720,1280);
mCaptureConfig.fps = 30;
boolean useCamera2 = false;
if(useCamera2){
mCapture = new KFVideoCaptureV2();
}else{
mCapture = new KFVideoCaptureV1();
}
mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
mCapture.startRunning();
}
private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
@Override
public void cameraOnOpened(){}
@Override
public void cameraOnClosed() {
}
@Override
public void cameraOnError(int error,String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onFrameAvailable(KFFrame frame) {
mRenderView.render((KFTextureFrame) frame);
}
};
}