1/* 2 * Copyright 2015 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11#import "RTCEAGLVideoView.h" 12 13#import <GLKit/GLKit.h> 14 15#import "RTCVideoFrame.h" 16#import "RTCOpenGLVideoRenderer.h" 17 18// RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen 19// refreshes, which should be 30fps. We wrap the display link in order to avoid 20// a retain cycle since CADisplayLink takes a strong reference onto its target. 21// The timer is paused by default. 22@interface RTCDisplayLinkTimer : NSObject 23 24@property(nonatomic) BOOL isPaused; 25 26- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler; 27- (void)invalidate; 28 29@end 30 31@implementation RTCDisplayLinkTimer { 32 CADisplayLink *_displayLink; 33 void (^_timerHandler)(void); 34} 35 36- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler { 37 NSParameterAssert(timerHandler); 38 if (self = [super init]) { 39 _timerHandler = timerHandler; 40 _displayLink = 41 [CADisplayLink displayLinkWithTarget:self 42 selector:@selector(displayLinkDidFire:)]; 43 _displayLink.paused = YES; 44 // Set to half of screen refresh, which should be 30fps. 45 [_displayLink setFrameInterval:2]; 46 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] 47 forMode:NSRunLoopCommonModes]; 48 } 49 return self; 50} 51 52- (void)dealloc { 53 [self invalidate]; 54} 55 56- (BOOL)isPaused { 57 return _displayLink.paused; 58} 59 60- (void)setIsPaused:(BOOL)isPaused { 61 _displayLink.paused = isPaused; 62} 63 64- (void)invalidate { 65 [_displayLink invalidate]; 66} 67 68- (void)displayLinkDidFire:(CADisplayLink *)displayLink { 69 _timerHandler(); 70} 71 72@end 73 74// RTCEAGLVideoView wraps a GLKView which is setup with 75// enableSetNeedsDisplay = NO for the purpose of gaining control of 76// exactly when to call -[GLKView display]. This need for extra 77// control is required to avoid triggering method calls on GLKView 78// that results in attempting to bind the underlying render buffer 79// when the drawable size would be empty which would result in the 80// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is 81// the method that will trigger the binding of the render 82// buffer. Because the standard behaviour of -[UIView setNeedsDisplay] 83// is disabled for the reasons above, the RTCEAGLVideoView maintains 84// its own |isDirty| flag. 85 86@interface RTCEAGLVideoView () <GLKViewDelegate> 87// |videoFrame| is set when we receive a frame from a worker thread and is read 88// from the display link callback so atomicity is required. 89@property(atomic, strong) RTCVideoFrame *videoFrame; 90@property(nonatomic, readonly) GLKView *glkView; 91@property(nonatomic, readonly) RTCOpenGLVideoRenderer *glRenderer; 92@end 93 94@implementation RTCEAGLVideoView { 95 RTCDisplayLinkTimer *_timer; 96 // This flag should only be set and read on the main thread (e.g. by 97 // setNeedsDisplay) 98 BOOL _isDirty; 99} 100 101@synthesize delegate = _delegate; 102@synthesize videoFrame = _videoFrame; 103@synthesize glkView = _glkView; 104@synthesize glRenderer = _glRenderer; 105 106- (instancetype)initWithFrame:(CGRect)frame { 107 if (self = [super initWithFrame:frame]) { 108 [self configure]; 109 } 110 return self; 111} 112 113- (instancetype)initWithCoder:(NSCoder *)aDecoder { 114 if (self = [super initWithCoder:aDecoder]) { 115 [self configure]; 116 } 117 return self; 118} 119 120- (void)configure { 121 EAGLContext *glContext = 122 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; 123 if (!glContext) { 124 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; 125 } 126 _glRenderer = [[RTCOpenGLVideoRenderer alloc] initWithContext:glContext]; 127 128 // GLKView manages a framebuffer for us. 129 _glkView = [[GLKView alloc] initWithFrame:CGRectZero 130 context:glContext]; 131 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; 132 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone; 133 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone; 134 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone; 135 _glkView.delegate = self; 136 _glkView.layer.masksToBounds = YES; 137 _glkView.enableSetNeedsDisplay = NO; 138 [self addSubview:_glkView]; 139 140 // Listen to application state in order to clean up OpenGL before app goes 141 // away. 142 NSNotificationCenter *notificationCenter = 143 [NSNotificationCenter defaultCenter]; 144 [notificationCenter addObserver:self 145 selector:@selector(willResignActive) 146 name:UIApplicationWillResignActiveNotification 147 object:nil]; 148 [notificationCenter addObserver:self 149 selector:@selector(didBecomeActive) 150 name:UIApplicationDidBecomeActiveNotification 151 object:nil]; 152 153 // Frames are received on a separate thread, so we poll for current frame 154 // using a refresh rate proportional to screen refresh frequency. This 155 // occurs on the main thread. 156 __weak RTCEAGLVideoView *weakSelf = self; 157 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{ 158 RTCEAGLVideoView *strongSelf = weakSelf; 159 [strongSelf displayLinkTimerDidFire]; 160 }]; 161 [self setupGL]; 162} 163 164- (void)dealloc { 165 [[NSNotificationCenter defaultCenter] removeObserver:self]; 166 UIApplicationState appState = 167 [UIApplication sharedApplication].applicationState; 168 if (appState == UIApplicationStateActive) { 169 [self teardownGL]; 170 } 171 [_timer invalidate]; 172} 173 174#pragma mark - UIView 175 176- (void)setNeedsDisplay { 177 [super setNeedsDisplay]; 178 _isDirty = YES; 179} 180 181- (void)setNeedsDisplayInRect:(CGRect)rect { 182 [super setNeedsDisplayInRect:rect]; 183 _isDirty = YES; 184} 185 186- (void)layoutSubviews { 187 [super layoutSubviews]; 188 _glkView.frame = self.bounds; 189} 190 191#pragma mark - GLKViewDelegate 192 193// This method is called when the GLKView's content is dirty and needs to be 194// redrawn. This occurs on main thread. 195- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { 196 // The renderer will draw the frame to the framebuffer corresponding to the 197 // one used by |view|. 198 [_glRenderer drawFrame:self.videoFrame]; 199} 200 201#pragma mark - RTCVideoRenderer 202 203// These methods may be called on non-main thread. 204- (void)setSize:(CGSize)size { 205 __weak RTCEAGLVideoView *weakSelf = self; 206 dispatch_async(dispatch_get_main_queue(), ^{ 207 RTCEAGLVideoView *strongSelf = weakSelf; 208 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size]; 209 }); 210} 211 212- (void)renderFrame:(RTCVideoFrame *)frame { 213 self.videoFrame = frame; 214} 215 216#pragma mark - Private 217 218- (void)displayLinkTimerDidFire { 219 // Don't render unless video frame have changed or the view content 220 // has explicitly been marked dirty. 221 if (!_isDirty && _glRenderer.lastDrawnFrame == self.videoFrame) { 222 return; 223 } 224 225 // Always reset isDirty at this point, even if -[GLKView display] 226 // won't be called in the case the drawable size is empty. 227 _isDirty = NO; 228 229 // Only call -[GLKView display] if the drawable size is 230 // non-empty. Calling display will make the GLKView setup its 231 // render buffer if necessary, but that will fail with error 232 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty. 233 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) { 234 [_glkView display]; 235 } 236} 237 238- (void)setupGL { 239 self.videoFrame = nil; 240 [_glRenderer setupGL]; 241 _timer.isPaused = NO; 242} 243 244- (void)teardownGL { 245 self.videoFrame = nil; 246 _timer.isPaused = YES; 247 [_glkView deleteDrawable]; 248 [_glRenderer teardownGL]; 249} 250 251- (void)didBecomeActive { 252 [self setupGL]; 253} 254 255- (void)willResignActive { 256 [self teardownGL]; 257} 258 259@end 260