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