blob: 53f5270884d34fff18d24f1e4dec31de8d1781e1 [file] [log] [blame]
Jon Hjellee799bad2016-01-11 13:47:11 -08001/*
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
tkchin9eeb6242016-04-27 01:54:20 -070011#import "WebRTC/RTCEAGLVideoView.h"
Jon Hjellee799bad2016-01-11 13:47:11 -080012
13#import <GLKit/GLKit.h>
14
magjed13941912017-05-30 06:11:58 -070015#import "RTCDefaultShader.h"
16#import "RTCI420TextureCache.h"
17#import "RTCNV12TextureCache.h"
magjed2f7f9b82017-04-13 04:15:53 -070018#import "WebRTC/RTCLogging.h"
magjedfb372f02016-08-10 07:58:29 -070019#import "WebRTC/RTCVideoFrame.h"
Anders Carlssone5960ce2017-06-22 15:26:30 +020020#import "WebRTC/RTCVideoFrameBuffer.h"
Jon Hjellee799bad2016-01-11 13:47:11 -080021
22// RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen
23// refreshes, which should be 30fps. We wrap the display link in order to avoid
24// a retain cycle since CADisplayLink takes a strong reference onto its target.
25// The timer is paused by default.
26@interface RTCDisplayLinkTimer : NSObject
27
28@property(nonatomic) BOOL isPaused;
29
30- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler;
31- (void)invalidate;
32
33@end
34
35@implementation RTCDisplayLinkTimer {
36 CADisplayLink *_displayLink;
37 void (^_timerHandler)(void);
38}
39
40- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler {
41 NSParameterAssert(timerHandler);
42 if (self = [super init]) {
43 _timerHandler = timerHandler;
44 _displayLink =
45 [CADisplayLink displayLinkWithTarget:self
46 selector:@selector(displayLinkDidFire:)];
47 _displayLink.paused = YES;
48 // Set to half of screen refresh, which should be 30fps.
49 [_displayLink setFrameInterval:2];
50 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
51 forMode:NSRunLoopCommonModes];
52 }
53 return self;
54}
55
56- (void)dealloc {
57 [self invalidate];
58}
59
60- (BOOL)isPaused {
61 return _displayLink.paused;
62}
63
64- (void)setIsPaused:(BOOL)isPaused {
65 _displayLink.paused = isPaused;
66}
67
68- (void)invalidate {
69 [_displayLink invalidate];
70}
71
72- (void)displayLinkDidFire:(CADisplayLink *)displayLink {
73 _timerHandler();
74}
75
76@end
77
78// RTCEAGLVideoView wraps a GLKView which is setup with
79// enableSetNeedsDisplay = NO for the purpose of gaining control of
80// exactly when to call -[GLKView display]. This need for extra
81// control is required to avoid triggering method calls on GLKView
82// that results in attempting to bind the underlying render buffer
83// when the drawable size would be empty which would result in the
84// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
85// the method that will trigger the binding of the render
86// buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
87// is disabled for the reasons above, the RTCEAGLVideoView maintains
88// its own |isDirty| flag.
89
90@interface RTCEAGLVideoView () <GLKViewDelegate>
91// |videoFrame| is set when we receive a frame from a worker thread and is read
92// from the display link callback so atomicity is required.
93@property(atomic, strong) RTCVideoFrame *videoFrame;
94@property(nonatomic, readonly) GLKView *glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -080095@end
96
97@implementation RTCEAGLVideoView {
98 RTCDisplayLinkTimer *_timer;
tkchin41a32872016-08-17 16:02:58 -070099 EAGLContext *_glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800100 // This flag should only be set and read on the main thread (e.g. by
101 // setNeedsDisplay)
102 BOOL _isDirty;
magjed13941912017-05-30 06:11:58 -0700103 id<RTCVideoViewShading> _shader;
104 RTCNV12TextureCache *_nv12TextureCache;
105 RTCI420TextureCache *_i420TextureCache;
magjed2f7f9b82017-04-13 04:15:53 -0700106 RTCVideoFrame *_lastDrawnFrame;
Jon Hjellee799bad2016-01-11 13:47:11 -0800107}
108
109@synthesize delegate = _delegate;
110@synthesize videoFrame = _videoFrame;
111@synthesize glkView = _glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -0800112
113- (instancetype)initWithFrame:(CGRect)frame {
magjed13941912017-05-30 06:11:58 -0700114 return [self initWithFrame:frame shader:[[RTCDefaultShader alloc] init]];
115}
116
117- (instancetype)initWithCoder:(NSCoder *)aDecoder {
118 return [self initWithCoder:aDecoder shader:[[RTCDefaultShader alloc] init]];
119}
120
121- (instancetype)initWithFrame:(CGRect)frame shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800122 if (self = [super initWithFrame:frame]) {
magjed13941912017-05-30 06:11:58 -0700123 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700124 if (![self configure]) {
125 return nil;
126 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800127 }
128 return self;
129}
130
magjed13941912017-05-30 06:11:58 -0700131- (instancetype)initWithCoder:(NSCoder *)aDecoder shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800132 if (self = [super initWithCoder:aDecoder]) {
magjed13941912017-05-30 06:11:58 -0700133 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700134 if (![self configure]) {
135 return nil;
136 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800137 }
138 return self;
139}
140
magjed1c12b812017-07-31 09:11:46 -0700141- (BOOL)configure {
Jon Hjellee799bad2016-01-11 13:47:11 -0800142 EAGLContext *glContext =
143 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
144 if (!glContext) {
145 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
146 }
magjed1c12b812017-07-31 09:11:46 -0700147 if (!glContext) {
148 RTCLogError(@"Failed to create EAGLContext");
149 return NO;
150 }
tkchin41a32872016-08-17 16:02:58 -0700151 _glContext = glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800152
153 // GLKView manages a framebuffer for us.
154 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700155 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800156 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
157 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
158 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
159 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
160 _glkView.delegate = self;
161 _glkView.layer.masksToBounds = YES;
162 _glkView.enableSetNeedsDisplay = NO;
163 [self addSubview:_glkView];
164
165 // Listen to application state in order to clean up OpenGL before app goes
166 // away.
167 NSNotificationCenter *notificationCenter =
168 [NSNotificationCenter defaultCenter];
169 [notificationCenter addObserver:self
170 selector:@selector(willResignActive)
171 name:UIApplicationWillResignActiveNotification
172 object:nil];
173 [notificationCenter addObserver:self
174 selector:@selector(didBecomeActive)
175 name:UIApplicationDidBecomeActiveNotification
176 object:nil];
177
178 // Frames are received on a separate thread, so we poll for current frame
179 // using a refresh rate proportional to screen refresh frequency. This
180 // occurs on the main thread.
181 __weak RTCEAGLVideoView *weakSelf = self;
182 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
183 RTCEAGLVideoView *strongSelf = weakSelf;
184 [strongSelf displayLinkTimerDidFire];
185 }];
Gustavo Garcia7281f922017-11-07 17:54:03 +0100186 if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
187 [self setupGL];
188 }
magjed1c12b812017-07-31 09:11:46 -0700189 return YES;
Jon Hjellee799bad2016-01-11 13:47:11 -0800190}
191
192- (void)dealloc {
193 [[NSNotificationCenter defaultCenter] removeObserver:self];
194 UIApplicationState appState =
195 [UIApplication sharedApplication].applicationState;
196 if (appState == UIApplicationStateActive) {
197 [self teardownGL];
198 }
199 [_timer invalidate];
tkchin41a32872016-08-17 16:02:58 -0700200 if (_glContext && [EAGLContext currentContext] == _glContext) {
201 [EAGLContext setCurrentContext:nil];
202 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800203}
204
205#pragma mark - UIView
206
207- (void)setNeedsDisplay {
208 [super setNeedsDisplay];
209 _isDirty = YES;
210}
211
212- (void)setNeedsDisplayInRect:(CGRect)rect {
213 [super setNeedsDisplayInRect:rect];
214 _isDirty = YES;
215}
216
217- (void)layoutSubviews {
218 [super layoutSubviews];
219 _glkView.frame = self.bounds;
220}
221
222#pragma mark - GLKViewDelegate
223
224// This method is called when the GLKView's content is dirty and needs to be
225// redrawn. This occurs on main thread.
226- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
227 // The renderer will draw the frame to the framebuffer corresponding to the
228 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700229 RTCVideoFrame *frame = self.videoFrame;
230 if (!frame || frame == _lastDrawnFrame) {
231 return;
232 }
233 [self ensureGLContext];
234 glClear(GL_COLOR_BUFFER_BIT);
Anders Carlssone5960ce2017-06-22 15:26:30 +0200235 if ([frame.buffer isKindOfClass:[RTCCVPixelBuffer class]]) {
magjed13941912017-05-30 06:11:58 -0700236 if (!_nv12TextureCache) {
237 _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
magjed2f7f9b82017-04-13 04:15:53 -0700238 }
magjed13941912017-05-30 06:11:58 -0700239 if (_nv12TextureCache) {
240 [_nv12TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200241 [_shader applyShadingForFrameWithWidth:frame.width
242 height:frame.height
243 rotation:frame.rotation
244 yPlane:_nv12TextureCache.yTexture
245 uvPlane:_nv12TextureCache.uvTexture];
magjed13941912017-05-30 06:11:58 -0700246 [_nv12TextureCache releaseTextures];
magjed2f7f9b82017-04-13 04:15:53 -0700247 }
magjed2f7f9b82017-04-13 04:15:53 -0700248 } else {
magjed13941912017-05-30 06:11:58 -0700249 if (!_i420TextureCache) {
250 _i420TextureCache = [[RTCI420TextureCache alloc] initWithContext:_glContext];
251 }
252 [_i420TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200253 [_shader applyShadingForFrameWithWidth:frame.width
254 height:frame.height
255 rotation:frame.rotation
256 yPlane:_i420TextureCache.yTexture
257 uPlane:_i420TextureCache.uTexture
258 vPlane:_i420TextureCache.vTexture];
magjed2f7f9b82017-04-13 04:15:53 -0700259 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800260}
261
262#pragma mark - RTCVideoRenderer
263
264// These methods may be called on non-main thread.
265- (void)setSize:(CGSize)size {
266 __weak RTCEAGLVideoView *weakSelf = self;
267 dispatch_async(dispatch_get_main_queue(), ^{
268 RTCEAGLVideoView *strongSelf = weakSelf;
269 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
270 });
271}
272
273- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800274 self.videoFrame = frame;
275}
276
277#pragma mark - Private
278
279- (void)displayLinkTimerDidFire {
280 // Don't render unless video frame have changed or the view content
281 // has explicitly been marked dirty.
magjed2f7f9b82017-04-13 04:15:53 -0700282 if (!_isDirty && _lastDrawnFrame == self.videoFrame) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800283 return;
284 }
285
286 // Always reset isDirty at this point, even if -[GLKView display]
287 // won't be called in the case the drawable size is empty.
288 _isDirty = NO;
289
290 // Only call -[GLKView display] if the drawable size is
291 // non-empty. Calling display will make the GLKView setup its
292 // render buffer if necessary, but that will fail with error
293 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
294 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
295 [_glkView display];
296 }
297}
298
299- (void)setupGL {
300 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700301 [self ensureGLContext];
302 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800303 _timer.isPaused = NO;
304}
305
306- (void)teardownGL {
307 self.videoFrame = nil;
308 _timer.isPaused = YES;
309 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700310 [self ensureGLContext];
magjed13941912017-05-30 06:11:58 -0700311 _nv12TextureCache = nil;
312 _i420TextureCache = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800313}
314
315- (void)didBecomeActive {
316 [self setupGL];
317}
318
319- (void)willResignActive {
320 [self teardownGL];
321}
322
magjed2f7f9b82017-04-13 04:15:53 -0700323- (void)ensureGLContext {
324 NSAssert(_glContext, @"context shouldn't be nil");
325 if ([EAGLContext currentContext] != _glContext) {
326 [EAGLContext setCurrentContext:_glContext];
327 }
328}
329
Jon Hjellee799bad2016-01-11 13:47:11 -0800330@end