blob: a26741479fa7d5bf618e5c86667404341ac7e1db [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;
Kári Tristan Helgasonccac9882018-05-30 16:44:21 +020048#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0
49 _displayLink.preferredFramesPerSecond = 30;
50#else
Jon Hjellee799bad2016-01-11 13:47:11 -080051 [_displayLink setFrameInterval:2];
Kári Tristan Helgasonccac9882018-05-30 16:44:21 +020052#endif
Jon Hjellee799bad2016-01-11 13:47:11 -080053 [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
54 forMode:NSRunLoopCommonModes];
55 }
56 return self;
57}
58
59- (void)dealloc {
60 [self invalidate];
61}
62
63- (BOOL)isPaused {
64 return _displayLink.paused;
65}
66
67- (void)setIsPaused:(BOOL)isPaused {
68 _displayLink.paused = isPaused;
69}
70
71- (void)invalidate {
72 [_displayLink invalidate];
73}
74
75- (void)displayLinkDidFire:(CADisplayLink *)displayLink {
76 _timerHandler();
77}
78
79@end
80
81// RTCEAGLVideoView wraps a GLKView which is setup with
82// enableSetNeedsDisplay = NO for the purpose of gaining control of
83// exactly when to call -[GLKView display]. This need for extra
84// control is required to avoid triggering method calls on GLKView
85// that results in attempting to bind the underlying render buffer
86// when the drawable size would be empty which would result in the
87// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
88// the method that will trigger the binding of the render
89// buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
90// is disabled for the reasons above, the RTCEAGLVideoView maintains
91// its own |isDirty| flag.
92
93@interface RTCEAGLVideoView () <GLKViewDelegate>
94// |videoFrame| is set when we receive a frame from a worker thread and is read
95// from the display link callback so atomicity is required.
96@property(atomic, strong) RTCVideoFrame *videoFrame;
97@property(nonatomic, readonly) GLKView *glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -080098@end
99
100@implementation RTCEAGLVideoView {
101 RTCDisplayLinkTimer *_timer;
tkchin41a32872016-08-17 16:02:58 -0700102 EAGLContext *_glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800103 // This flag should only be set and read on the main thread (e.g. by
104 // setNeedsDisplay)
105 BOOL _isDirty;
magjed13941912017-05-30 06:11:58 -0700106 id<RTCVideoViewShading> _shader;
107 RTCNV12TextureCache *_nv12TextureCache;
108 RTCI420TextureCache *_i420TextureCache;
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300109 // As timestamps should be unique between frames, will store last
110 // drawn frame timestamp instead of the whole frame to reduce memory usage.
111 int64_t _lastDrawnFrameTimeStampNs;
Jon Hjellee799bad2016-01-11 13:47:11 -0800112}
113
114@synthesize delegate = _delegate;
115@synthesize videoFrame = _videoFrame;
116@synthesize glkView = _glkView;
Jon Hjellee799bad2016-01-11 13:47:11 -0800117
118- (instancetype)initWithFrame:(CGRect)frame {
magjed13941912017-05-30 06:11:58 -0700119 return [self initWithFrame:frame shader:[[RTCDefaultShader alloc] init]];
120}
121
122- (instancetype)initWithCoder:(NSCoder *)aDecoder {
123 return [self initWithCoder:aDecoder shader:[[RTCDefaultShader alloc] init]];
124}
125
126- (instancetype)initWithFrame:(CGRect)frame shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800127 if (self = [super initWithFrame:frame]) {
magjed13941912017-05-30 06:11:58 -0700128 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700129 if (![self configure]) {
130 return nil;
131 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800132 }
133 return self;
134}
135
magjed13941912017-05-30 06:11:58 -0700136- (instancetype)initWithCoder:(NSCoder *)aDecoder shader:(id<RTCVideoViewShading>)shader {
Jon Hjellee799bad2016-01-11 13:47:11 -0800137 if (self = [super initWithCoder:aDecoder]) {
magjed13941912017-05-30 06:11:58 -0700138 _shader = shader;
magjed1c12b812017-07-31 09:11:46 -0700139 if (![self configure]) {
140 return nil;
141 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800142 }
143 return self;
144}
145
magjed1c12b812017-07-31 09:11:46 -0700146- (BOOL)configure {
Jon Hjellee799bad2016-01-11 13:47:11 -0800147 EAGLContext *glContext =
148 [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
149 if (!glContext) {
150 glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
151 }
magjed1c12b812017-07-31 09:11:46 -0700152 if (!glContext) {
153 RTCLogError(@"Failed to create EAGLContext");
154 return NO;
155 }
tkchin41a32872016-08-17 16:02:58 -0700156 _glContext = glContext;
Jon Hjellee799bad2016-01-11 13:47:11 -0800157
158 // GLKView manages a framebuffer for us.
159 _glkView = [[GLKView alloc] initWithFrame:CGRectZero
tkchin41a32872016-08-17 16:02:58 -0700160 context:_glContext];
Jon Hjellee799bad2016-01-11 13:47:11 -0800161 _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
162 _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
163 _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
164 _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
165 _glkView.delegate = self;
166 _glkView.layer.masksToBounds = YES;
167 _glkView.enableSetNeedsDisplay = NO;
168 [self addSubview:_glkView];
169
170 // Listen to application state in order to clean up OpenGL before app goes
171 // away.
172 NSNotificationCenter *notificationCenter =
173 [NSNotificationCenter defaultCenter];
174 [notificationCenter addObserver:self
175 selector:@selector(willResignActive)
176 name:UIApplicationWillResignActiveNotification
177 object:nil];
178 [notificationCenter addObserver:self
179 selector:@selector(didBecomeActive)
180 name:UIApplicationDidBecomeActiveNotification
181 object:nil];
182
183 // Frames are received on a separate thread, so we poll for current frame
184 // using a refresh rate proportional to screen refresh frequency. This
185 // occurs on the main thread.
186 __weak RTCEAGLVideoView *weakSelf = self;
187 _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
188 RTCEAGLVideoView *strongSelf = weakSelf;
189 [strongSelf displayLinkTimerDidFire];
190 }];
Gustavo Garcia7281f922017-11-07 17:54:03 +0100191 if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
192 [self setupGL];
193 }
magjed1c12b812017-07-31 09:11:46 -0700194 return YES;
Jon Hjellee799bad2016-01-11 13:47:11 -0800195}
196
197- (void)dealloc {
198 [[NSNotificationCenter defaultCenter] removeObserver:self];
199 UIApplicationState appState =
200 [UIApplication sharedApplication].applicationState;
201 if (appState == UIApplicationStateActive) {
202 [self teardownGL];
203 }
204 [_timer invalidate];
Gustavo Garcia730add82018-01-04 02:45:38 +0100205 [self ensureGLContext];
206 _shader = nil;
tkchin41a32872016-08-17 16:02:58 -0700207 if (_glContext && [EAGLContext currentContext] == _glContext) {
208 [EAGLContext setCurrentContext:nil];
209 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800210}
211
212#pragma mark - UIView
213
214- (void)setNeedsDisplay {
215 [super setNeedsDisplay];
216 _isDirty = YES;
217}
218
219- (void)setNeedsDisplayInRect:(CGRect)rect {
220 [super setNeedsDisplayInRect:rect];
221 _isDirty = YES;
222}
223
224- (void)layoutSubviews {
225 [super layoutSubviews];
226 _glkView.frame = self.bounds;
227}
228
229#pragma mark - GLKViewDelegate
230
231// This method is called when the GLKView's content is dirty and needs to be
232// redrawn. This occurs on main thread.
233- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
234 // The renderer will draw the frame to the framebuffer corresponding to the
235 // one used by |view|.
magjed2f7f9b82017-04-13 04:15:53 -0700236 RTCVideoFrame *frame = self.videoFrame;
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300237 if (!frame || frame.timeStampNs == _lastDrawnFrameTimeStampNs) {
magjed2f7f9b82017-04-13 04:15:53 -0700238 return;
239 }
240 [self ensureGLContext];
241 glClear(GL_COLOR_BUFFER_BIT);
Anders Carlssone5960ce2017-06-22 15:26:30 +0200242 if ([frame.buffer isKindOfClass:[RTCCVPixelBuffer class]]) {
magjed13941912017-05-30 06:11:58 -0700243 if (!_nv12TextureCache) {
244 _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
magjed2f7f9b82017-04-13 04:15:53 -0700245 }
magjed13941912017-05-30 06:11:58 -0700246 if (_nv12TextureCache) {
247 [_nv12TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200248 [_shader applyShadingForFrameWithWidth:frame.width
249 height:frame.height
250 rotation:frame.rotation
251 yPlane:_nv12TextureCache.yTexture
252 uvPlane:_nv12TextureCache.uvTexture];
magjed13941912017-05-30 06:11:58 -0700253 [_nv12TextureCache releaseTextures];
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300254
255 _lastDrawnFrameTimeStampNs = self.videoFrame.timeStampNs;
magjed2f7f9b82017-04-13 04:15:53 -0700256 }
magjed2f7f9b82017-04-13 04:15:53 -0700257 } else {
magjed13941912017-05-30 06:11:58 -0700258 if (!_i420TextureCache) {
259 _i420TextureCache = [[RTCI420TextureCache alloc] initWithContext:_glContext];
260 }
261 [_i420TextureCache uploadFrameToTextures:frame];
Magnus Jedvert6b9653e2017-06-05 17:58:34 +0200262 [_shader applyShadingForFrameWithWidth:frame.width
263 height:frame.height
264 rotation:frame.rotation
265 yPlane:_i420TextureCache.yTexture
266 uPlane:_i420TextureCache.uTexture
267 vPlane:_i420TextureCache.vTexture];
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300268
269 _lastDrawnFrameTimeStampNs = self.videoFrame.timeStampNs;
magjed2f7f9b82017-04-13 04:15:53 -0700270 }
Jon Hjellee799bad2016-01-11 13:47:11 -0800271}
272
273#pragma mark - RTCVideoRenderer
274
275// These methods may be called on non-main thread.
276- (void)setSize:(CGSize)size {
277 __weak RTCEAGLVideoView *weakSelf = self;
278 dispatch_async(dispatch_get_main_queue(), ^{
279 RTCEAGLVideoView *strongSelf = weakSelf;
280 [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
281 });
282}
283
284- (void)renderFrame:(RTCVideoFrame *)frame {
Jon Hjellee799bad2016-01-11 13:47:11 -0800285 self.videoFrame = frame;
286}
287
288#pragma mark - Private
289
290- (void)displayLinkTimerDidFire {
291 // Don't render unless video frame have changed or the view content
292 // has explicitly been marked dirty.
Maxim Pavlova72b7fc2018-04-10 16:57:43 +0300293 if (!_isDirty && _lastDrawnFrameTimeStampNs == self.videoFrame.timeStampNs) {
Jon Hjellee799bad2016-01-11 13:47:11 -0800294 return;
295 }
296
297 // Always reset isDirty at this point, even if -[GLKView display]
298 // won't be called in the case the drawable size is empty.
299 _isDirty = NO;
300
301 // Only call -[GLKView display] if the drawable size is
302 // non-empty. Calling display will make the GLKView setup its
303 // render buffer if necessary, but that will fail with error
304 // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
305 if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
306 [_glkView display];
307 }
308}
309
310- (void)setupGL {
311 self.videoFrame = nil;
magjed2f7f9b82017-04-13 04:15:53 -0700312 [self ensureGLContext];
313 glDisable(GL_DITHER);
Jon Hjellee799bad2016-01-11 13:47:11 -0800314 _timer.isPaused = NO;
315}
316
317- (void)teardownGL {
318 self.videoFrame = nil;
319 _timer.isPaused = YES;
320 [_glkView deleteDrawable];
magjed2f7f9b82017-04-13 04:15:53 -0700321 [self ensureGLContext];
magjed13941912017-05-30 06:11:58 -0700322 _nv12TextureCache = nil;
323 _i420TextureCache = nil;
Jon Hjellee799bad2016-01-11 13:47:11 -0800324}
325
326- (void)didBecomeActive {
327 [self setupGL];
328}
329
330- (void)willResignActive {
331 [self teardownGL];
332}
333
magjed2f7f9b82017-04-13 04:15:53 -0700334- (void)ensureGLContext {
335 NSAssert(_glContext, @"context shouldn't be nil");
336 if ([EAGLContext currentContext] != _glContext) {
337 [EAGLContext setCurrentContext:_glContext];
338 }
339}
340
Jon Hjellee799bad2016-01-11 13:47:11 -0800341@end