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