blob: d39c59876407acde5d3639d37be52091cf3c1f88 [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
tkchin9eeb6242016-04-27 01:54:20 -070015#import "RTCOpenGLVideoRenderer.h"
magjedfb372f02016-08-10 07:58:29 -070016#import "WebRTC/RTCVideoFrame.h"
Jon Hjellee799bad2016-01-11 13:47:11 -080017
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;
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;
100}
101
102@synthesize delegate = _delegate;
103@synthesize videoFrame = _videoFrame;
104@synthesize glkView = _glkView;
105@synthesize glRenderer = _glRenderer;
106
107- (instancetype)initWithFrame:(CGRect)frame {
108 if (self = [super initWithFrame:frame]) {
109 [self configure];
110 }
111 return self;
112}
113
114- (instancetype)initWithCoder:(NSCoder *)aDecoder {
115 if (self = [super initWithCoder:aDecoder]) {
116 [self configure];
117 }
118 return self;
119}
120
121- (void)configure {
122 EAGLContext *glContext =
123 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
124 if (!glContext) {
125 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
126 }
tkchin41a32872016-08-17 16:02:58 -0700127 _glContext = glContext;
128 _glRenderer = [[RTCOpenGLVideoRenderer alloc] initWithContext:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800129
130 // GLKView manages a framebuffer for us.
131 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700132 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800133 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
134 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
135 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
136 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
137 _glkView.delegate = self;
138 _glkView.layer.masksToBounds = YES;
139 _glkView.enableSetNeedsDisplay = NO;
140 [self addSubview:_glkView];
141
142 // Listen to application state in order to clean up OpenGL before app goes
143 // away.
144 NSNotificationCenter *notificationCenter =
145 [NSNotificationCenter defaultCenter];
146 [notificationCenter addObserver:self
147 selector:@selector(willResignActive)
148 name:UIApplicationWillResignActiveNotification
149 object:nil];
150 [notificationCenter addObserver:self
151 selector:@selector(didBecomeActive)
152 name:UIApplicationDidBecomeActiveNotification
153 object:nil];
154
155 // Frames are received on a separate thread, so we poll for current frame
156 // using a refresh rate proportional to screen refresh frequency. This
157 // occurs on the main thread.
158 __weak RTCEAGLVideoView *weakSelf = self;
159 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
160 RTCEAGLVideoView *strongSelf = weakSelf;
161 [strongSelf displayLinkTimerDidFire];
162 }];
163 [self setupGL];
164}
165
166- (void)dealloc {
167 [[NSNotificationCenter defaultCenter] removeObserver:self];
168 UIApplicationState appState =
169 [UIApplication sharedApplication].applicationState;
170 if (appState == UIApplicationStateActive) {
171 [self teardownGL];
172 }
173 [_timer invalidate];
tkchin41a32872016-08-17 16:02:58 -0700174 if (_glContext && [EAGLContext currentContext] == _glContext) {
175 [EAGLContext setCurrentContext:nil];
176 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800177}
178
179#pragma mark - UIView
180
181- (void)setNeedsDisplay {
182 [super setNeedsDisplay];
183 _isDirty = YES;
184}
185
186- (void)setNeedsDisplayInRect:(CGRect)rect {
187 [super setNeedsDisplayInRect:rect];
188 _isDirty = YES;
189}
190
191- (void)layoutSubviews {
192 [super layoutSubviews];
193 _glkView.frame = self.bounds;
194}
195
196#pragma mark - GLKViewDelegate
197
198// This method is called when the GLKView's content is dirty and needs to be
199// redrawn. This occurs on main thread.
200- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
201 // The renderer will draw the frame to the framebuffer corresponding to the
202 // one used by |view|.
203 [_glRenderer drawFrame:self.videoFrame];
204}
205
206#pragma mark - RTCVideoRenderer
207
208// These methods may be called on non-main thread.
209- (void)setSize:(CGSize)size {
210 __weak RTCEAGLVideoView *weakSelf = self;
211 dispatch_async(dispatch_get_main_queue(), ^{
212 RTCEAGLVideoView *strongSelf = weakSelf;
213 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
214 });
215}
216
217- (void)renderFrame:(RTCVideoFrame *)frame {
tkchin04dbb342016-08-08 03:10:07 -0700218#if !TARGET_OS_IPHONE
tkchin7d06a8c2016-04-04 14:10:43 -0700219 // Generate the i420 frame on video send thread instead of main thread.
220 // TODO(tkchin): Remove this once RTCEAGLVideoView supports uploading
tkchin04dbb342016-08-08 03:10:07 -0700221 // CVPixelBuffer textures on OSX.
tkchin7d06a8c2016-04-04 14:10:43 -0700222 [frame convertBufferIfNeeded];
tkchin04dbb342016-08-08 03:10:07 -0700223#endif
Jon Hjellee799bad2016-01-11 13:47:11 -0800224 self.videoFrame = frame;
225}
226
227#pragma mark - Private
228
229- (void)displayLinkTimerDidFire {
230 // Don't render unless video frame have changed or the view content
231 // has explicitly been marked dirty.
232 if (!_isDirty && _glRenderer.lastDrawnFrame == self.videoFrame) {
233 return;
234 }
235
236 // Always reset isDirty at this point, even if -[GLKView display]
237 // won't be called in the case the drawable size is empty.
238 _isDirty = NO;
239
240 // Only call -[GLKView display] if the drawable size is
241 // non-empty. Calling display will make the GLKView setup its
242 // render buffer if necessary, but that will fail with error
243 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
244 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
245 [_glkView display];
246 }
247}
248
249- (void)setupGL {
250 self.videoFrame = nil;
251 [_glRenderer setupGL];
252 _timer.isPaused = NO;
253}
254
255- (void)teardownGL {
256 self.videoFrame = nil;
257 _timer.isPaused = YES;
258 [_glkView deleteDrawable];
259 [_glRenderer teardownGL];
260}
261
262- (void)didBecomeActive {
263 [self setupGL];
264}
265
266- (void)willResignActive {
267 [self teardownGL];
268}
269
270@end