1 /* 2 * libjingle 3 * Copyright 2014 Google Inc. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. The name of the author may not be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28 #if !defined(__has_feature) || !__has_feature(objc_arc) 29 #error "This file requires ARC support." 30 #endif 31 32 #import "RTCEAGLVideoView.h" 33 34 #import <GLKit/GLKit.h> 35 36 #import "RTCI420Frame.h" 37 #import "RTCOpenGLVideoRenderer.h" 38 39 // RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen 40 // refreshes, which should be 30fps. We wrap the display link in order to avoid 41 // a retain cycle since CADisplayLink takes a strong reference onto its target. 42 // The timer is paused by default. 43 @interface RTCDisplayLinkTimer : NSObject 44 45 @property(nonatomic) BOOL isPaused; 46 47 - (instancetype)initWithTimerHandler:(void (^)(void))timerHandler; 48 - (void)invalidate; 49 50 @end 51 52 @implementation RTCDisplayLinkTimer { 53 CADisplayLink* _displayLink; 54 void (^_timerHandler)(void); 55 } 56 57 - (instancetype)initWithTimerHandler:(void (^)(void))timerHandler { 58 NSParameterAssert(timerHandler); 59 if (self = [super init]) { 60 _timerHandler = timerHandler; 61 _displayLink = 62 [CADisplayLink displayLinkWithTarget:self 63 selector:@selector(displayLinkDidFire:)]; 64 _displayLink.paused = YES; 65 // Set to half of screen refresh, which should be 30fps. 66 [_displayLink setFrameInterval:2]; 67 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] 68 forMode:NSRunLoopCommonModes]; 69 } 70 return self; 71 } 72 73 - (void)dealloc { 74 [self invalidate]; 75 } 76 77 - (BOOL)isPaused { 78 return _displayLink.paused; 79 } 80 81 - (void)setIsPaused:(BOOL)isPaused { 82 _displayLink.paused = isPaused; 83 } 84 85 - (void)invalidate { 86 [_displayLink invalidate]; 87 } 88 89 - (void)displayLinkDidFire:(CADisplayLink*)displayLink { 90 _timerHandler(); 91 } 92 93 @end 94 95 // RTCEAGLVideoView wraps a GLKView which is setup with 96 // enableSetNeedsDisplay = NO for the purpose of gaining control of 97 // exactly when to call -[GLKView display]. This need for extra 98 // control is required to avoid triggering method calls on GLKView 99 // that results in attempting to bind the underlying render buffer 100 // when the drawable size would be empty which would result in the 101 // error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is 102 // the method that will trigger the binding of the render 103 // buffer. Because the standard behaviour of -[UIView setNeedsDisplay] 104 // is disabled for the reasons above, the RTCEAGLVideoView maintains 105 // its own |isDirty| flag. 106 107 @interface RTCEAGLVideoView () <GLKViewDelegate> 108 // |i420Frame| is set when we receive a frame from a worker thread and is read 109 // from the display link callback so atomicity is required. 110 @property(atomic, strong) RTCI420Frame* i420Frame; 111 @property(nonatomic, readonly) GLKView* glkView; 112 @property(nonatomic, readonly) RTCOpenGLVideoRenderer* glRenderer; 113 @end 114 115 @implementation RTCEAGLVideoView { 116 RTCDisplayLinkTimer* _timer; 117 GLKView* _glkView; 118 RTCOpenGLVideoRenderer* _glRenderer; 119 // This flag should only be set and read on the main thread (e.g. by 120 // setNeedsDisplay) 121 BOOL _isDirty; 122 } 123 124 - (instancetype)initWithFrame:(CGRect)frame { 125 if (self = [super initWithFrame:frame]) { 126 [self configure]; 127 } 128 return self; 129 } 130 131 - (instancetype)initWithCoder:(NSCoder *)aDecoder { 132 if (self = [super initWithCoder:aDecoder]) { 133 [self configure]; 134 } 135 return self; 136 } 137 138 - (void)configure { 139 EAGLContext* glContext = 140 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; 141 if (!glContext) { 142 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; 143 } 144 _glRenderer = [[RTCOpenGLVideoRenderer alloc] initWithContext:glContext]; 145 146 // GLKView manages a framebuffer for us. 147 _glkView = [[GLKView alloc] initWithFrame:CGRectZero 148 context:glContext]; 149 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; 150 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone; 151 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone; 152 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone; 153 _glkView.delegate = self; 154 _glkView.layer.masksToBounds = YES; 155 _glkView.enableSetNeedsDisplay = NO; 156 [self addSubview:_glkView]; 157 158 // Listen to application state in order to clean up OpenGL before app goes 159 // away. 160 NSNotificationCenter* notificationCenter = 161 [NSNotificationCenter defaultCenter]; 162 [notificationCenter addObserver:self 163 selector:@selector(willResignActive) 164 name:UIApplicationWillResignActiveNotification 165 object:nil]; 166 [notificationCenter addObserver:self 167 selector:@selector(didBecomeActive) 168 name:UIApplicationDidBecomeActiveNotification 169 object:nil]; 170 171 // Frames are received on a separate thread, so we poll for current frame 172 // using a refresh rate proportional to screen refresh frequency. This 173 // occurs on the main thread. 174 __weak RTCEAGLVideoView* weakSelf = self; 175 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{ 176 RTCEAGLVideoView* strongSelf = weakSelf; 177 [strongSelf displayLinkTimerDidFire]; 178 }]; 179 [self setupGL]; 180 } 181 182 - (void)dealloc { 183 [[NSNotificationCenter defaultCenter] removeObserver:self]; 184 UIApplicationState appState = 185 [UIApplication sharedApplication].applicationState; 186 if (appState == UIApplicationStateActive) { 187 [self teardownGL]; 188 } 189 [_timer invalidate]; 190 } 191 192 #pragma mark - UIView 193 194 - (void)setNeedsDisplay { 195 [super setNeedsDisplay]; 196 _isDirty = YES; 197 } 198 199 - (void)setNeedsDisplayInRect:(CGRect)rect { 200 [super setNeedsDisplayInRect:rect]; 201 _isDirty = YES; 202 } 203 204 - (void)layoutSubviews { 205 [super layoutSubviews]; 206 _glkView.frame = self.bounds; 207 } 208 209 #pragma mark - GLKViewDelegate 210 211 // This method is called when the GLKView's content is dirty and needs to be 212 // redrawn. This occurs on main thread. 213 - (void)glkView:(GLKView*)view drawInRect:(CGRect)rect { 214 // The renderer will draw the frame to the framebuffer corresponding to the 215 // one used by |view|. 216 [_glRenderer drawFrame:self.i420Frame]; 217 } 218 219 #pragma mark - RTCVideoRenderer 220 221 // These methods may be called on non-main thread. 222 - (void)setSize:(CGSize)size { 223 __weak RTCEAGLVideoView* weakSelf = self; 224 dispatch_async(dispatch_get_main_queue(), ^{ 225 RTCEAGLVideoView* strongSelf = weakSelf; 226 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size]; 227 }); 228 } 229 230 - (void)renderFrame:(RTCI420Frame*)frame { 231 self.i420Frame = frame; 232 } 233 234 #pragma mark - Private 235 236 - (void)displayLinkTimerDidFire { 237 // Don't render unless video frame have changed or the view content 238 // has explicitly been marked dirty. 239 if (!_isDirty && _glRenderer.lastDrawnFrame == self.i420Frame) { 240 return; 241 } 242 243 // Always reset isDirty at this point, even if -[GLKView display] 244 // won't be called in the case the drawable size is empty. 245 _isDirty = NO; 246 247 // Only call -[GLKView display] if the drawable size is 248 // non-empty. Calling display will make the GLKView setup its 249 // render buffer if necessary, but that will fail with error 250 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty. 251 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) { 252 [_glkView display]; 253 } 254 } 255 256 - (void)setupGL { 257 self.i420Frame = nil; 258 [_glRenderer setupGL]; 259 _timer.isPaused = NO; 260 } 261 262 - (void)teardownGL { 263 self.i420Frame = nil; 264 _timer.isPaused = YES; 265 [_glkView deleteDrawable]; 266 [_glRenderer teardownGL]; 267 } 268 269 - (void)didBecomeActive { 270 [self setupGL]; 271 } 272 273 - (void)willResignActive { 274 [self teardownGL]; 275 } 276 277 @end 278