blob: 8f379c864aec22f83127c93174bd7ac93a14d61b [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 }];
Gustavo Garcia7281f922017-11-07 17:54:03 +0100186 if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
187 [self setupGL];
188 }
magjed1c12b812017-07-31 09:11:46 -0700189 return YES;
Jon Hjellee799bad2016-01-11 13:47:11 -0800190}
191
192- (void)dealloc {
193 [[NSNotificationCenter defaultCenter] removeObserver:self];
194 UIApplicationState appState =
195 [UIApplication sharedApplication].applicationState;
196 if (appState == UIApplicationStateActive) {
197 [self teardownGL];
198 }
199 [_timer invalidate];
Gustavo Garcia730add82018-01-04 02:45:38 +0100200 [self ensureGLContext];
201 _shader = nil;
tkchin41a32872016-08-17 16:02:58 -0700202 if (_glContext && [EAGLContext currentContext] == _glContext) {
203 [EAGLContext setCurrentContext:nil];
204 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800205}
206
207#pragma mark - UIView
208
209- (void)setNeedsDisplay {
210 [super setNeedsDisplay];
211 _isDirty = YES;
212}
213
214- (void)setNeedsDisplayInRect:(CGRect)rect {
215 [super setNeedsDisplayInRect:rect];
216 _isDirty = YES;
217}
218
219- (void)layoutSubviews {
220 [super layoutSubviews];
221 _glkView.frame = self.bounds;
222}
223
224#pragma mark - GLKViewDelegate
225
226// This method is called when the GLKView's content is dirty and needs to be
227// redrawn. This occurs on main thread.
228- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
229 // The renderer will draw the frame to the framebuffer corresponding to the
230 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700231 RTCVideoFrame *frame = self.videoFrame;
232 if (!frame || frame == _lastDrawnFrame) {
233 return;
234 }
235 [self ensureGLContext];
236 glClear(GL_COLOR_BUFFER_BIT);
Anders Carlssone5960ce2017-06-22 15:26:30 +0200237 if ([frame.buffer isKindOfClass:[RTCCVPixelBuffer class]]) {
magjed13941912017-05-30 06:11:58 -0700238 if (!_nv12TextureCache) {
239 _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
magjed2f7f9b82017-04-13 04:15:53 -0700240 }
magjed13941912017-05-30 06:11:58 -0700241 if (_nv12TextureCache) {
242 [_nv12TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200243 [_shader applyShadingForFrameWithWidth:frame.width
244 height:frame.height
245 rotation:frame.rotation
246 yPlane:_nv12TextureCache.yTexture
247 uvPlane:_nv12TextureCache.uvTexture];
magjed13941912017-05-30 06:11:58 -0700248 [_nv12TextureCache releaseTextures];
magjed2f7f9b82017-04-13 04:15:53 -0700249 }
magjed2f7f9b82017-04-13 04:15:53 -0700250 } else {
magjed13941912017-05-30 06:11:58 -0700251 if (!_i420TextureCache) {
252 _i420TextureCache = [[RTCI420TextureCache alloc] initWithContext:_glContext];
253 }
254 [_i420TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200255 [_shader applyShadingForFrameWithWidth:frame.width
256 height:frame.height
257 rotation:frame.rotation
258 yPlane:_i420TextureCache.yTexture
259 uPlane:_i420TextureCache.uTexture
260 vPlane:_i420TextureCache.vTexture];
magjed2f7f9b82017-04-13 04:15:53 -0700261 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800262}
263
264#pragma mark - RTCVideoRenderer
265
266// These methods may be called on non-main thread.
267- (void)setSize:(CGSize)size {
268 __weak RTCEAGLVideoView *weakSelf = self;
269 dispatch_async(dispatch_get_main_queue(), ^{
270 RTCEAGLVideoView *strongSelf = weakSelf;
271 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
272 });
273}
274
275- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800276 self.videoFrame = frame;
277}
278
279#pragma mark - Private
280
281- (void)displayLinkTimerDidFire {
282 // Don't render unless video frame have changed or the view content
283 // has explicitly been marked dirty.
magjed2f7f9b82017-04-13 04:15:53 -0700284 if (!_isDirty && _lastDrawnFrame == self.videoFrame) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800285 return;
286 }
287
288 // Always reset isDirty at this point, even if -[GLKView display]
289 // won't be called in the case the drawable size is empty.
290 _isDirty = NO;
291
292 // Only call -[GLKView display] if the drawable size is
293 // non-empty. Calling display will make the GLKView setup its
294 // render buffer if necessary, but that will fail with error
295 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
296 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
297 [_glkView display];
298 }
299}
300
301- (void)setupGL {
302 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700303 [self ensureGLContext];
304 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800305 _timer.isPaused = NO;
306}
307
308- (void)teardownGL {
309 self.videoFrame = nil;
310 _timer.isPaused = YES;
311 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700312 [self ensureGLContext];
magjed13941912017-05-30 06:11:58 -0700313 _nv12TextureCache = nil;
314 _i420TextureCache = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800315}
316
317- (void)didBecomeActive {
318 [self setupGL];
319}
320
321- (void)willResignActive {
322 [self teardownGL];
323}
324
magjed2f7f9b82017-04-13 04:15:53 -0700325- (void)ensureGLContext {
326 NSAssert(_glContext, @"context shouldn't be nil");
327 if ([EAGLContext currentContext] != _glContext) {
328 [EAGLContext setCurrentContext:_glContext];
329 }
330}
331
Jon Hjellee799bad2016-01-11 13:47:11 -0800332@end