blob: 80282f0b0b49dfefa1d59ec7a22caf2f9e5b5364 [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
magjed13941912017-05-30 06:11:58 -070015#import "RTCDefaultShader.h"
16#import "RTCI420TextureCache.h"
17#import "RTCNV12TextureCache.h"
magjed2f7f9b82017-04-13 04:15:53 -070018#import "WebRTC/RTCLogging.h"
magjedfb372f02016-08-10 07:58:29 -070019#import "WebRTC/RTCVideoFrame.h"
Anders Carlssone5960ce2017-06-22 15:26:30 +020020#import "WebRTC/RTCVideoFrameBuffer.h"
Jon Hjellee799bad2016-01-11 13:47:11 -080021
22// RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen
23// refreshes, which should be 30fps. We wrap the display link in order to avoid
24// a retain cycle since CADisplayLink takes a strong reference onto its target.
25// The timer is paused by default.
26@interface RTCDisplayLinkTimer : NSObject
27
28@property(nonatomic) BOOL isPaused;
29
30- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler;
31- (void)invalidate;
32
33@end
34
35@implementation RTCDisplayLinkTimer {
36 CADisplayLink *_displayLink;
37 void (^_timerHandler)(void);
38}
39
40- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler {
41 NSParameterAssert(timerHandler);
42 if (self = [super init]) {
43 _timerHandler = timerHandler;
44 _displayLink =
45 [CADisplayLink displayLinkWithTarget:self
46 selector:@selector(displayLinkDidFire:)];
47 _displayLink.paused = YES;
48 // Set to half of screen refresh, which should be 30fps.
49 [_displayLink setFrameInterval:2];
50 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
51 forMode:NSRunLoopCommonModes];
52 }
53 return self;
54}
55
56- (void)dealloc {
57 [self invalidate];
58}
59
60- (BOOL)isPaused {
61 return _displayLink.paused;
62}
63
64- (void)setIsPaused:(BOOL)isPaused {
65 _displayLink.paused = isPaused;
66}
67
68- (void)invalidate {
69 [_displayLink invalidate];
70}
71
72- (void)displayLinkDidFire:(CADisplayLink *)displayLink {
73 _timerHandler();
74}
75
76@end
77
78// RTCEAGLVideoView wraps a GLKView which is setup with
79// enableSetNeedsDisplay = NO for the purpose of gaining control of
80// exactly when to call -[GLKView display]. This need for extra
81// control is required to avoid triggering method calls on GLKView
82// that results in attempting to bind the underlying render buffer
83// when the drawable size would be empty which would result in the
84// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
85// the method that will trigger the binding of the render
86// buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
87// is disabled for the reasons above, the RTCEAGLVideoView maintains
88// its own |isDirty| flag.
89
90@interface RTCEAGLVideoView () <GLKViewDelegate>
91// |videoFrame| is set when we receive a frame from a worker thread and is read
92// from the display link callback so atomicity is required.
93@property(atomic, strong) RTCVideoFrame *videoFrame;
94@property(nonatomic, readonly) GLKView *glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -080095@end
96
97@implementation RTCEAGLVideoView {
98 RTCDisplayLinkTimer *_timer;
tkchin41a32872016-08-17 16:02:58 -070099 EAGLContext *_glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800100 // This flag should only be set and read on the main thread (e.g. by
101 // setNeedsDisplay)
102 BOOL _isDirty;
magjed13941912017-05-30 06:11:58 -0700103 id<RTCVideoViewShading> _shader;
104 RTCNV12TextureCache *_nv12TextureCache;
105 RTCI420TextureCache *_i420TextureCache;
magjed2f7f9b82017-04-13 04:15:53 -0700106 RTCVideoFrame *_lastDrawnFrame;
Jon Hjellee799bad2016-01-11 13:47:11 -0800107}
108
109@synthesize delegate = _delegate;
110@synthesize videoFrame = _videoFrame;
111@synthesize glkView = _glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -0800112
113- (instancetype)initWithFrame:(CGRect)frame {
magjed13941912017-05-30 06:11:58 -0700114 return [self initWithFrame:frame shader:[[RTCDefaultShader alloc] init]];
115}
116
117- (instancetype)initWithCoder:(NSCoder *)aDecoder {
118 return [self initWithCoder:aDecoder shader:[[RTCDefaultShader alloc] init]];
119}
120
121- (instancetype)initWithFrame:(CGRect)frame shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800122 if (self = [super initWithFrame:frame]) {
magjed13941912017-05-30 06:11:58 -0700123 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700124 if (![self configure]) {
125 return nil;
126 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800127 }
128 return self;
129}
130
magjed13941912017-05-30 06:11:58 -0700131- (instancetype)initWithCoder:(NSCoder *)aDecoder shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800132 if (self = [super initWithCoder:aDecoder]) {
magjed13941912017-05-30 06:11:58 -0700133 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700134 if (![self configure]) {
135 return nil;
136 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800137 }
138 return self;
139}
140
magjed1c12b812017-07-31 09:11:46 -0700141- (BOOL)configure {
Jon Hjellee799bad2016-01-11 13:47:11 -0800142 EAGLContext *glContext =
143 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
144 if (!glContext) {
145 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
146 }
magjed1c12b812017-07-31 09:11:46 -0700147 if (!glContext) {
148 RTCLogError(@"Failed to create EAGLContext");
149 return NO;
150 }
tkchin41a32872016-08-17 16:02:58 -0700151 _glContext = glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800152
153 // GLKView manages a framebuffer for us.
154 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700155 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800156 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
157 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
158 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
159 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
160 _glkView.delegate = self;
161 _glkView.layer.masksToBounds = YES;
162 _glkView.enableSetNeedsDisplay = NO;
163 [self addSubview:_glkView];
164
165 // Listen to application state in order to clean up OpenGL before app goes
166 // away.
167 NSNotificationCenter *notificationCenter =
168 [NSNotificationCenter defaultCenter];
169 [notificationCenter addObserver:self
170 selector:@selector(willResignActive)
171 name:UIApplicationWillResignActiveNotification
172 object:nil];
173 [notificationCenter addObserver:self
174 selector:@selector(didBecomeActive)
175 name:UIApplicationDidBecomeActiveNotification
176 object:nil];
177
178 // Frames are received on a separate thread, so we poll for current frame
179 // using a refresh rate proportional to screen refresh frequency. This
180 // occurs on the main thread.
181 __weak RTCEAGLVideoView *weakSelf = self;
182 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
183 RTCEAGLVideoView *strongSelf = weakSelf;
184 [strongSelf displayLinkTimerDidFire];
185 }];
186 [self setupGL];
magjed1c12b812017-07-31 09:11:46 -0700187 return YES;
Jon Hjellee799bad2016-01-11 13:47:11 -0800188}
189
190- (void)dealloc {
191 [[NSNotificationCenter defaultCenter] removeObserver:self];
192 UIApplicationState appState =
193 [UIApplication sharedApplication].applicationState;
194 if (appState == UIApplicationStateActive) {
195 [self teardownGL];
196 }
197 [_timer invalidate];
tkchin41a32872016-08-17 16:02:58 -0700198 if (_glContext && [EAGLContext currentContext] == _glContext) {
199 [EAGLContext setCurrentContext:nil];
200 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800201}
202
203#pragma mark - UIView
204
205- (void)setNeedsDisplay {
206 [super setNeedsDisplay];
207 _isDirty = YES;
208}
209
210- (void)setNeedsDisplayInRect:(CGRect)rect {
211 [super setNeedsDisplayInRect:rect];
212 _isDirty = YES;
213}
214
215- (void)layoutSubviews {
216 [super layoutSubviews];
217 _glkView.frame = self.bounds;
218}
219
220#pragma mark - GLKViewDelegate
221
222// This method is called when the GLKView's content is dirty and needs to be
223// redrawn. This occurs on main thread.
224- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
225 // The renderer will draw the frame to the framebuffer corresponding to the
226 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700227 RTCVideoFrame *frame = self.videoFrame;
228 if (!frame || frame == _lastDrawnFrame) {
229 return;
230 }
231 [self ensureGLContext];
232 glClear(GL_COLOR_BUFFER_BIT);
Anders Carlssone5960ce2017-06-22 15:26:30 +0200233 if ([frame.buffer isKindOfClass:[RTCCVPixelBuffer class]]) {
magjed13941912017-05-30 06:11:58 -0700234 if (!_nv12TextureCache) {
235 _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
magjed2f7f9b82017-04-13 04:15:53 -0700236 }
magjed13941912017-05-30 06:11:58 -0700237 if (_nv12TextureCache) {
238 [_nv12TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200239 [_shader applyShadingForFrameWithWidth:frame.width
240 height:frame.height
241 rotation:frame.rotation
242 yPlane:_nv12TextureCache.yTexture
243 uvPlane:_nv12TextureCache.uvTexture];
magjed13941912017-05-30 06:11:58 -0700244 [_nv12TextureCache releaseTextures];
magjed2f7f9b82017-04-13 04:15:53 -0700245 }
magjed2f7f9b82017-04-13 04:15:53 -0700246 } else {
magjed13941912017-05-30 06:11:58 -0700247 if (!_i420TextureCache) {
248 _i420TextureCache = [[RTCI420TextureCache alloc] initWithContext:_glContext];
249 }
250 [_i420TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200251 [_shader applyShadingForFrameWithWidth:frame.width
252 height:frame.height
253 rotation:frame.rotation
254 yPlane:_i420TextureCache.yTexture
255 uPlane:_i420TextureCache.uTexture
256 vPlane:_i420TextureCache.vTexture];
magjed2f7f9b82017-04-13 04:15:53 -0700257 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800258}
259
260#pragma mark - RTCVideoRenderer
261
262// These methods may be called on non-main thread.
263- (void)setSize:(CGSize)size {
264 __weak RTCEAGLVideoView *weakSelf = self;
265 dispatch_async(dispatch_get_main_queue(), ^{
266 RTCEAGLVideoView *strongSelf = weakSelf;
267 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
268 });
269}
270
271- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800272 self.videoFrame = frame;
273}
274
275#pragma mark - Private
276
277- (void)displayLinkTimerDidFire {
278 // Don't render unless video frame have changed or the view content
279 // has explicitly been marked dirty.
magjed2f7f9b82017-04-13 04:15:53 -0700280 if (!_isDirty && _lastDrawnFrame == self.videoFrame) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800281 return;
282 }
283
284 // Always reset isDirty at this point, even if -[GLKView display]
285 // won't be called in the case the drawable size is empty.
286 _isDirty = NO;
287
288 // Only call -[GLKView display] if the drawable size is
289 // non-empty. Calling display will make the GLKView setup its
290 // render buffer if necessary, but that will fail with error
291 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
292 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
293 [_glkView display];
294 }
295}
296
297- (void)setupGL {
298 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700299 [self ensureGLContext];
300 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800301 _timer.isPaused = NO;
302}
303
304- (void)teardownGL {
305 self.videoFrame = nil;
306 _timer.isPaused = YES;
307 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700308 [self ensureGLContext];
magjed13941912017-05-30 06:11:58 -0700309 _nv12TextureCache = nil;
310 _i420TextureCache = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800311}
312
313- (void)didBecomeActive {
314 [self setupGL];
315}
316
317- (void)willResignActive {
318 [self teardownGL];
319}
320
magjed2f7f9b82017-04-13 04:15:53 -0700321- (void)ensureGLContext {
322 NSAssert(_glContext, @"context shouldn't be nil");
323 if ([EAGLContext currentContext] != _glContext) {
324 [EAGLContext setCurrentContext:_glContext];
325 }
326}
327
Jon Hjellee799bad2016-01-11 13:47:11 -0800328@end