blob: 1fb03bc909e27305eee7eb41bd9f6066983f7e3e [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
magjed2f7f9b82017-04-13 04:15:53 -070015#import "RTCShader+Private.h"
16#import "WebRTC/RTCLogging.h"
magjedfb372f02016-08-10 07:58:29 -070017#import "WebRTC/RTCVideoFrame.h"
Jon Hjellee799bad2016-01-11 13:47:11 -080018
19// RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen
20// refreshes, which should be 30fps. We wrap the display link in order to avoid
21// a retain cycle since CADisplayLink takes a strong reference onto its target.
22// The timer is paused by default.
23@interface RTCDisplayLinkTimer : NSObject
24
25@property(nonatomic) BOOL isPaused;
26
27- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler;
28- (void)invalidate;
29
30@end
31
32@implementation RTCDisplayLinkTimer {
33 CADisplayLink *_displayLink;
34 void (^_timerHandler)(void);
35}
36
37- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler {
38 NSParameterAssert(timerHandler);
39 if (self = [super init]) {
40 _timerHandler = timerHandler;
41 _displayLink =
42 [CADisplayLink displayLinkWithTarget:self
43 selector:@selector(displayLinkDidFire:)];
44 _displayLink.paused = YES;
45 // Set to half of screen refresh, which should be 30fps.
46 [_displayLink setFrameInterval:2];
47 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
48 forMode:NSRunLoopCommonModes];
49 }
50 return self;
51}
52
53- (void)dealloc {
54 [self invalidate];
55}
56
57- (BOOL)isPaused {
58 return _displayLink.paused;
59}
60
61- (void)setIsPaused:(BOOL)isPaused {
62 _displayLink.paused = isPaused;
63}
64
65- (void)invalidate {
66 [_displayLink invalidate];
67}
68
69- (void)displayLinkDidFire:(CADisplayLink *)displayLink {
70 _timerHandler();
71}
72
73@end
74
75// RTCEAGLVideoView wraps a GLKView which is setup with
76// enableSetNeedsDisplay = NO for the purpose of gaining control of
77// exactly when to call -[GLKView display]. This need for extra
78// control is required to avoid triggering method calls on GLKView
79// that results in attempting to bind the underlying render buffer
80// when the drawable size would be empty which would result in the
81// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
82// the method that will trigger the binding of the render
83// buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
84// is disabled for the reasons above, the RTCEAGLVideoView maintains
85// its own |isDirty| flag.
86
87@interface RTCEAGLVideoView () <GLKViewDelegate>
88// |videoFrame| is set when we receive a frame from a worker thread and is read
89// from the display link callback so atomicity is required.
90@property(atomic, strong) RTCVideoFrame *videoFrame;
91@property(nonatomic, readonly) GLKView *glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -080092@end
93
94@implementation RTCEAGLVideoView {
95 RTCDisplayLinkTimer *_timer;
tkchin41a32872016-08-17 16:02:58 -070096 EAGLContext *_glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -080097 // This flag should only be set and read on the main thread (e.g. by
98 // setNeedsDisplay)
99 BOOL _isDirty;
magjed2f7f9b82017-04-13 04:15:53 -0700100 id<RTCShader> _i420Shader;
101 id<RTCShader> _nv12Shader;
102 RTCVideoFrame *_lastDrawnFrame;
Jon Hjellee799bad2016-01-11 13:47:11 -0800103}
104
105@synthesize delegate = _delegate;
106@synthesize videoFrame = _videoFrame;
107@synthesize glkView = _glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -0800108
109- (instancetype)initWithFrame:(CGRect)frame {
110 if (self = [super initWithFrame:frame]) {
111 [self configure];
112 }
113 return self;
114}
115
116- (instancetype)initWithCoder:(NSCoder *)aDecoder {
117 if (self = [super initWithCoder:aDecoder]) {
118 [self configure];
119 }
120 return self;
121}
122
123- (void)configure {
124 EAGLContext *glContext =
125 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
126 if (!glContext) {
127 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
128 }
tkchin41a32872016-08-17 16:02:58 -0700129 _glContext = glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800130
131 // GLKView manages a framebuffer for us.
132 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700133 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800134 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
135 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
136 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
137 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
138 _glkView.delegate = self;
139 _glkView.layer.masksToBounds = YES;
140 _glkView.enableSetNeedsDisplay = NO;
141 [self addSubview:_glkView];
142
143 // Listen to application state in order to clean up OpenGL before app goes
144 // away.
145 NSNotificationCenter *notificationCenter =
146 [NSNotificationCenter defaultCenter];
147 [notificationCenter addObserver:self
148 selector:@selector(willResignActive)
149 name:UIApplicationWillResignActiveNotification
150 object:nil];
151 [notificationCenter addObserver:self
152 selector:@selector(didBecomeActive)
153 name:UIApplicationDidBecomeActiveNotification
154 object:nil];
155
156 // Frames are received on a separate thread, so we poll for current frame
157 // using a refresh rate proportional to screen refresh frequency. This
158 // occurs on the main thread.
159 __weak RTCEAGLVideoView *weakSelf = self;
160 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
161 RTCEAGLVideoView *strongSelf = weakSelf;
162 [strongSelf displayLinkTimerDidFire];
163 }];
164 [self setupGL];
165}
166
167- (void)dealloc {
168 [[NSNotificationCenter defaultCenter] removeObserver:self];
169 UIApplicationState appState =
170 [UIApplication sharedApplication].applicationState;
171 if (appState == UIApplicationStateActive) {
172 [self teardownGL];
173 }
174 [_timer invalidate];
tkchin41a32872016-08-17 16:02:58 -0700175 if (_glContext && [EAGLContext currentContext] == _glContext) {
176 [EAGLContext setCurrentContext:nil];
177 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800178}
179
180#pragma mark - UIView
181
182- (void)setNeedsDisplay {
183 [super setNeedsDisplay];
184 _isDirty = YES;
185}
186
187- (void)setNeedsDisplayInRect:(CGRect)rect {
188 [super setNeedsDisplayInRect:rect];
189 _isDirty = YES;
190}
191
192- (void)layoutSubviews {
193 [super layoutSubviews];
194 _glkView.frame = self.bounds;
195}
196
197#pragma mark - GLKViewDelegate
198
199// This method is called when the GLKView's content is dirty and needs to be
200// redrawn. This occurs on main thread.
201- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
202 // The renderer will draw the frame to the framebuffer corresponding to the
203 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700204 RTCVideoFrame *frame = self.videoFrame;
205 if (!frame || frame == _lastDrawnFrame) {
206 return;
207 }
208 [self ensureGLContext];
209 glClear(GL_COLOR_BUFFER_BIT);
210 id<RTCShader> shader = nil;
211 if (frame.nativeHandle) {
212 if (!_nv12Shader) {
213 _nv12Shader = [[RTCNativeNV12Shader alloc] initWithContext:_glContext];
214 }
215 shader = _nv12Shader;
216 } else {
217 if (!_i420Shader) {
218 _i420Shader = [[RTCI420Shader alloc] initWithContext:_glContext];
219 }
220 shader = _i420Shader;
221 }
222 if (shader && [shader drawFrame:frame]) {
223 _lastDrawnFrame = frame;
224 } else {
225 RTCLog(@"Failed to draw frame.");
226 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800227}
228
229#pragma mark - RTCVideoRenderer
230
231// These methods may be called on non-main thread.
232- (void)setSize:(CGSize)size {
233 __weak RTCEAGLVideoView *weakSelf = self;
234 dispatch_async(dispatch_get_main_queue(), ^{
235 RTCEAGLVideoView *strongSelf = weakSelf;
236 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
237 });
238}
239
240- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800241 self.videoFrame = frame;
242}
243
244#pragma mark - Private
245
246- (void)displayLinkTimerDidFire {
247 // Don't render unless video frame have changed or the view content
248 // has explicitly been marked dirty.
magjed2f7f9b82017-04-13 04:15:53 -0700249 if (!_isDirty && _lastDrawnFrame == self.videoFrame) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800250 return;
251 }
252
253 // Always reset isDirty at this point, even if -[GLKView display]
254 // won't be called in the case the drawable size is empty.
255 _isDirty = NO;
256
257 // Only call -[GLKView display] if the drawable size is
258 // non-empty. Calling display will make the GLKView setup its
259 // render buffer if necessary, but that will fail with error
260 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
261 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
262 [_glkView display];
263 }
264}
265
266- (void)setupGL {
267 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700268 [self ensureGLContext];
269 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800270 _timer.isPaused = NO;
271}
272
273- (void)teardownGL {
274 self.videoFrame = nil;
275 _timer.isPaused = YES;
276 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700277 [self ensureGLContext];
278 _i420Shader = nil;
279 _nv12Shader = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800280}
281
282- (void)didBecomeActive {
283 [self setupGL];
284}
285
286- (void)willResignActive {
287 [self teardownGL];
288}
289
magjed2f7f9b82017-04-13 04:15:53 -0700290- (void)ensureGLContext {
291 NSAssert(_glContext, @"context shouldn't be nil");
292 if ([EAGLContext currentContext] != _glContext) {
293 [EAGLContext setCurrentContext:_glContext];
294 }
295}
296
Jon Hjellee799bad2016-01-11 13:47:11 -0800297@end