blob: 7655dd650b216a8b1d64a1d17a1974269d648fa7 [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;
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300106 // As timestamps should be unique between frames, will store last
107 // drawn frame timestamp instead of the whole frame to reduce memory usage.
108 int64_t _lastDrawnFrameTimeStampNs;
Jon Hjellee799bad2016-01-11 13:47:11 -0800109}
110
111@synthesize delegate = _delegate;
112@synthesize videoFrame = _videoFrame;
113@synthesize glkView = _glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -0800114
115- (instancetype)initWithFrame:(CGRect)frame {
magjed13941912017-05-30 06:11:58 -0700116 return [self initWithFrame:frame shader:[[RTCDefaultShader alloc] init]];
117}
118
119- (instancetype)initWithCoder:(NSCoder *)aDecoder {
120 return [self initWithCoder:aDecoder shader:[[RTCDefaultShader alloc] init]];
121}
122
123- (instancetype)initWithFrame:(CGRect)frame shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800124 if (self = [super initWithFrame:frame]) {
magjed13941912017-05-30 06:11:58 -0700125 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700126 if (![self configure]) {
127 return nil;
128 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800129 }
130 return self;
131}
132
magjed13941912017-05-30 06:11:58 -0700133- (instancetype)initWithCoder:(NSCoder *)aDecoder shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800134 if (self = [super initWithCoder:aDecoder]) {
magjed13941912017-05-30 06:11:58 -0700135 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700136 if (![self configure]) {
137 return nil;
138 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800139 }
140 return self;
141}
142
magjed1c12b812017-07-31 09:11:46 -0700143- (BOOL)configure {
Jon Hjellee799bad2016-01-11 13:47:11 -0800144 EAGLContext *glContext =
145 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
146 if (!glContext) {
147 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
148 }
magjed1c12b812017-07-31 09:11:46 -0700149 if (!glContext) {
150 RTCLogError(@"Failed to create EAGLContext");
151 return NO;
152 }
tkchin41a32872016-08-17 16:02:58 -0700153 _glContext = glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800154
155 // GLKView manages a framebuffer for us.
156 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700157 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800158 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
159 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
160 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
161 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
162 _glkView.delegate = self;
163 _glkView.layer.masksToBounds = YES;
164 _glkView.enableSetNeedsDisplay = NO;
165 [self addSubview:_glkView];
166
167 // Listen to application state in order to clean up OpenGL before app goes
168 // away.
169 NSNotificationCenter *notificationCenter =
170 [NSNotificationCenter defaultCenter];
171 [notificationCenter addObserver:self
172 selector:@selector(willResignActive)
173 name:UIApplicationWillResignActiveNotification
174 object:nil];
175 [notificationCenter addObserver:self
176 selector:@selector(didBecomeActive)
177 name:UIApplicationDidBecomeActiveNotification
178 object:nil];
179
180 // Frames are received on a separate thread, so we poll for current frame
181 // using a refresh rate proportional to screen refresh frequency. This
182 // occurs on the main thread.
183 __weak RTCEAGLVideoView *weakSelf = self;
184 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
185 RTCEAGLVideoView *strongSelf = weakSelf;
186 [strongSelf displayLinkTimerDidFire];
187 }];
Gustavo Garcia7281f922017-11-07 17:54:03 +0100188 if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
189 [self setupGL];
190 }
magjed1c12b812017-07-31 09:11:46 -0700191 return YES;
Jon Hjellee799bad2016-01-11 13:47:11 -0800192}
193
194- (void)dealloc {
195 [[NSNotificationCenter defaultCenter] removeObserver:self];
196 UIApplicationState appState =
197 [UIApplication sharedApplication].applicationState;
198 if (appState == UIApplicationStateActive) {
199 [self teardownGL];
200 }
201 [_timer invalidate];
Gustavo Garcia730add82018-01-04 02:45:38 +0100202 [self ensureGLContext];
203 _shader = nil;
tkchin41a32872016-08-17 16:02:58 -0700204 if (_glContext && [EAGLContext currentContext] == _glContext) {
205 [EAGLContext setCurrentContext:nil];
206 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800207}
208
209#pragma mark - UIView
210
211- (void)setNeedsDisplay {
212 [super setNeedsDisplay];
213 _isDirty = YES;
214}
215
216- (void)setNeedsDisplayInRect:(CGRect)rect {
217 [super setNeedsDisplayInRect:rect];
218 _isDirty = YES;
219}
220
221- (void)layoutSubviews {
222 [super layoutSubviews];
223 _glkView.frame = self.bounds;
224}
225
226#pragma mark - GLKViewDelegate
227
228// This method is called when the GLKView's content is dirty and needs to be
229// redrawn. This occurs on main thread.
230- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
231 // The renderer will draw the frame to the framebuffer corresponding to the
232 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700233 RTCVideoFrame *frame = self.videoFrame;
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300234 if (!frame || frame.timeStampNs == _lastDrawnFrameTimeStampNs) {
magjed2f7f9b82017-04-13 04:15:53 -0700235 return;
236 }
237 [self ensureGLContext];
238 glClear(GL_COLOR_BUFFER_BIT);
Anders Carlssone5960ce2017-06-22 15:26:30 +0200239 if ([frame.buffer isKindOfClass:[RTCCVPixelBuffer class]]) {
magjed13941912017-05-30 06:11:58 -0700240 if (!_nv12TextureCache) {
241 _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
magjed2f7f9b82017-04-13 04:15:53 -0700242 }
magjed13941912017-05-30 06:11:58 -0700243 if (_nv12TextureCache) {
244 [_nv12TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200245 [_shader applyShadingForFrameWithWidth:frame.width
246 height:frame.height
247 rotation:frame.rotation
248 yPlane:_nv12TextureCache.yTexture
249 uvPlane:_nv12TextureCache.uvTexture];
magjed13941912017-05-30 06:11:58 -0700250 [_nv12TextureCache releaseTextures];
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300251
252 _lastDrawnFrameTimeStampNs = self.videoFrame.timeStampNs;
magjed2f7f9b82017-04-13 04:15:53 -0700253 }
magjed2f7f9b82017-04-13 04:15:53 -0700254 } else {
magjed13941912017-05-30 06:11:58 -0700255 if (!_i420TextureCache) {
256 _i420TextureCache = [[RTCI420TextureCache alloc] initWithContext:_glContext];
257 }
258 [_i420TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200259 [_shader applyShadingForFrameWithWidth:frame.width
260 height:frame.height
261 rotation:frame.rotation
262 yPlane:_i420TextureCache.yTexture
263 uPlane:_i420TextureCache.uTexture
264 vPlane:_i420TextureCache.vTexture];
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300265
266 _lastDrawnFrameTimeStampNs = self.videoFrame.timeStampNs;
magjed2f7f9b82017-04-13 04:15:53 -0700267 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800268}
269
270#pragma mark - RTCVideoRenderer
271
272// These methods may be called on non-main thread.
273- (void)setSize:(CGSize)size {
274 __weak RTCEAGLVideoView *weakSelf = self;
275 dispatch_async(dispatch_get_main_queue(), ^{
276 RTCEAGLVideoView *strongSelf = weakSelf;
277 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
278 });
279}
280
281- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800282 self.videoFrame = frame;
283}
284
285#pragma mark - Private
286
287- (void)displayLinkTimerDidFire {
288 // Don't render unless video frame have changed or the view content
289 // has explicitly been marked dirty.
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300290 if (!_isDirty && _lastDrawnFrameTimeStampNs == self.videoFrame.timeStampNs) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800291 return;
292 }
293
294 // Always reset isDirty at this point, even if -[GLKView display]
295 // won't be called in the case the drawable size is empty.
296 _isDirty = NO;
297
298 // Only call -[GLKView display] if the drawable size is
299 // non-empty. Calling display will make the GLKView setup its
300 // render buffer if necessary, but that will fail with error
301 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
302 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
303 [_glkView display];
304 }
305}
306
307- (void)setupGL {
308 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700309 [self ensureGLContext];
310 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800311 _timer.isPaused = NO;
312}
313
314- (void)teardownGL {
315 self.videoFrame = nil;
316 _timer.isPaused = YES;
317 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700318 [self ensureGLContext];
magjed13941912017-05-30 06:11:58 -0700319 _nv12TextureCache = nil;
320 _i420TextureCache = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800321}
322
323- (void)didBecomeActive {
324 [self setupGL];
325}
326
327- (void)willResignActive {
328 [self teardownGL];
329}
330
magjed2f7f9b82017-04-13 04:15:53 -0700331- (void)ensureGLContext {
332 NSAssert(_glContext, @"context shouldn't be nil");
333 if ([EAGLContext currentContext] != _glContext) {
334 [EAGLContext setCurrentContext:_glContext];
335 }
336}
337
Jon Hjellee799bad2016-01-11 13:47:11 -0800338@end