OLD | NEW |
| (Empty) |
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 "webrtc/api/objc/RTCEAGLVideoView.h" | |
12 | |
13 #import <GLKit/GLKit.h> | |
14 | |
15 #import "webrtc/api/objc/RTCOpenGLVideoRenderer.h" | |
16 #import "webrtc/api/objc/RTCVideoFrame.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 // Generate the i420 frame on video send thread instead of main thread. | |
214 // TODO(tkchin): Remove this once RTCEAGLVideoView supports uploading | |
215 // CVPixelBuffer textures. | |
216 [frame convertBufferIfNeeded]; | |
217 self.videoFrame = frame; | |
218 } | |
219 | |
220 #pragma mark - Private | |
221 | |
222 - (void)displayLinkTimerDidFire { | |
223 // Don't render unless video frame have changed or the view content | |
224 // has explicitly been marked dirty. | |
225 if (!_isDirty && _glRenderer.lastDrawnFrame == self.videoFrame) { | |
226 return; | |
227 } | |
228 | |
229 // Always reset isDirty at this point, even if -[GLKView display] | |
230 // won't be called in the case the drawable size is empty. | |
231 _isDirty = NO; | |
232 | |
233 // Only call -[GLKView display] if the drawable size is | |
234 // non-empty. Calling display will make the GLKView setup its | |
235 // render buffer if necessary, but that will fail with error | |
236 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty. | |
237 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) { | |
238 [_glkView display]; | |
239 } | |
240 } | |
241 | |
242 - (void)setupGL { | |
243 self.videoFrame = nil; | |
244 [_glRenderer setupGL]; | |
245 _timer.isPaused = NO; | |
246 } | |
247 | |
248 - (void)teardownGL { | |
249 self.videoFrame = nil; | |
250 _timer.isPaused = YES; | |
251 [_glkView deleteDrawable]; | |
252 [_glRenderer teardownGL]; | |
253 } | |
254 | |
255 - (void)didBecomeActive { | |
256 [self setupGL]; | |
257 } | |
258 | |
259 - (void)willResignActive { | |
260 [self teardownGL]; | |
261 } | |
262 | |
263 @end | |
OLD | NEW |