Home | History | Annotate | Download | only in objc
      1 /*
      2  * libjingle
      3  * Copyright 2014 Google Inc.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are met:
      7  *
      8  *  1. Redistributions of source code must retain the above copyright notice,
      9  *     this list of conditions and the following disclaimer.
     10  *  2. Redistributions in binary form must reproduce the above copyright notice,
     11  *     this list of conditions and the following disclaimer in the documentation
     12  *     and/or other materials provided with the distribution.
     13  *  3. The name of the author may not be used to endorse or promote products
     14  *     derived from this software without specific prior written permission.
     15  *
     16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
     17  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     18  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
     19  * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     20  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
     22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
     23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
     24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
     25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     26  */
     27 
     28 #if !defined(__has_feature) || !__has_feature(objc_arc)
     29 #error "This file requires ARC support."
     30 #endif
     31 
     32 #import "RTCEAGLVideoView.h"
     33 
     34 #import <GLKit/GLKit.h>
     35 
     36 #import "RTCI420Frame.h"
     37 #import "RTCOpenGLVideoRenderer.h"
     38 
     39 // RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen
     40 // refreshes, which should be 30fps. We wrap the display link in order to avoid
     41 // a retain cycle since CADisplayLink takes a strong reference onto its target.
     42 // The timer is paused by default.
     43 @interface RTCDisplayLinkTimer : NSObject
     44 
     45 @property(nonatomic) BOOL isPaused;
     46 
     47 - (instancetype)initWithTimerHandler:(void (^)(void))timerHandler;
     48 - (void)invalidate;
     49 
     50 @end
     51 
     52 @implementation RTCDisplayLinkTimer {
     53   CADisplayLink* _displayLink;
     54   void (^_timerHandler)(void);
     55 }
     56 
     57 - (instancetype)initWithTimerHandler:(void (^)(void))timerHandler {
     58   NSParameterAssert(timerHandler);
     59   if (self = [super init]) {
     60     _timerHandler = timerHandler;
     61     _displayLink =
     62         [CADisplayLink displayLinkWithTarget:self
     63                                     selector:@selector(displayLinkDidFire:)];
     64     _displayLink.paused = YES;
     65     // Set to half of screen refresh, which should be 30fps.
     66     [_displayLink setFrameInterval:2];
     67     [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
     68                        forMode:NSRunLoopCommonModes];
     69   }
     70   return self;
     71 }
     72 
     73 - (void)dealloc {
     74   [self invalidate];
     75 }
     76 
     77 - (BOOL)isPaused {
     78   return _displayLink.paused;
     79 }
     80 
     81 - (void)setIsPaused:(BOOL)isPaused {
     82   _displayLink.paused = isPaused;
     83 }
     84 
     85 - (void)invalidate {
     86   [_displayLink invalidate];
     87 }
     88 
     89 - (void)displayLinkDidFire:(CADisplayLink*)displayLink {
     90   _timerHandler();
     91 }
     92 
     93 @end
     94 
     95 // RTCEAGLVideoView wraps a GLKView which is setup with
     96 // enableSetNeedsDisplay = NO for the purpose of gaining control of
     97 // exactly when to call -[GLKView display]. This need for extra
     98 // control is required to avoid triggering method calls on GLKView
     99 // that results in attempting to bind the underlying render buffer
    100 // when the drawable size would be empty which would result in the
    101 // error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
    102 // the method that will trigger the binding of the render
    103 // buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
    104 // is disabled for the reasons above, the RTCEAGLVideoView maintains
    105 // its own |isDirty| flag.
    106 
    107 @interface RTCEAGLVideoView () <GLKViewDelegate>
    108 // |i420Frame| is set when we receive a frame from a worker thread and is read
    109 // from the display link callback so atomicity is required.
    110 @property(atomic, strong) RTCI420Frame* i420Frame;
    111 @property(nonatomic, readonly) GLKView* glkView;
    112 @property(nonatomic, readonly) RTCOpenGLVideoRenderer* glRenderer;
    113 @end
    114 
    115 @implementation RTCEAGLVideoView {
    116   RTCDisplayLinkTimer* _timer;
    117   GLKView* _glkView;
    118   RTCOpenGLVideoRenderer* _glRenderer;
    119   // This flag should only be set and read on the main thread (e.g. by
    120   // setNeedsDisplay)
    121   BOOL _isDirty;
    122 }
    123 
    124 - (instancetype)initWithFrame:(CGRect)frame {
    125   if (self = [super initWithFrame:frame]) {
    126     [self configure];
    127   }
    128   return self;
    129 }
    130 
    131 - (instancetype)initWithCoder:(NSCoder *)aDecoder {
    132   if (self = [super initWithCoder:aDecoder]) {
    133     [self configure];
    134   }
    135   return self;
    136 }
    137 
    138 - (void)configure {
    139   EAGLContext* glContext =
    140     [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    141   if (!glContext) {
    142     glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    143   }
    144   _glRenderer = [[RTCOpenGLVideoRenderer alloc] initWithContext:glContext];
    145 
    146   // GLKView manages a framebuffer for us.
    147   _glkView = [[GLKView alloc] initWithFrame:CGRectZero
    148                                     context:glContext];
    149   _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    150   _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
    151   _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
    152   _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
    153   _glkView.delegate = self;
    154   _glkView.layer.masksToBounds = YES;
    155   _glkView.enableSetNeedsDisplay = NO;
    156   [self addSubview:_glkView];
    157 
    158   // Listen to application state in order to clean up OpenGL before app goes
    159   // away.
    160   NSNotificationCenter* notificationCenter =
    161     [NSNotificationCenter defaultCenter];
    162   [notificationCenter addObserver:self
    163                          selector:@selector(willResignActive)
    164                              name:UIApplicationWillResignActiveNotification
    165                            object:nil];
    166   [notificationCenter addObserver:self
    167                          selector:@selector(didBecomeActive)
    168                              name:UIApplicationDidBecomeActiveNotification
    169                            object:nil];
    170 
    171   // Frames are received on a separate thread, so we poll for current frame
    172   // using a refresh rate proportional to screen refresh frequency. This
    173   // occurs on the main thread.
    174   __weak RTCEAGLVideoView* weakSelf = self;
    175   _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
    176       RTCEAGLVideoView* strongSelf = weakSelf;
    177       [strongSelf displayLinkTimerDidFire];
    178     }];
    179   [self setupGL];
    180 }
    181 
    182 - (void)dealloc {
    183   [[NSNotificationCenter defaultCenter] removeObserver:self];
    184   UIApplicationState appState =
    185       [UIApplication sharedApplication].applicationState;
    186   if (appState == UIApplicationStateActive) {
    187     [self teardownGL];
    188   }
    189   [_timer invalidate];
    190 }
    191 
    192 #pragma mark - UIView
    193 
    194 - (void)setNeedsDisplay {
    195   [super setNeedsDisplay];
    196   _isDirty = YES;
    197 }
    198 
    199 - (void)setNeedsDisplayInRect:(CGRect)rect {
    200   [super setNeedsDisplayInRect:rect];
    201   _isDirty = YES;
    202 }
    203 
    204 - (void)layoutSubviews {
    205   [super layoutSubviews];
    206   _glkView.frame = self.bounds;
    207 }
    208 
    209 #pragma mark - GLKViewDelegate
    210 
    211 // This method is called when the GLKView's content is dirty and needs to be
    212 // redrawn. This occurs on main thread.
    213 - (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {
    214   // The renderer will draw the frame to the framebuffer corresponding to the
    215   // one used by |view|.
    216   [_glRenderer drawFrame:self.i420Frame];
    217 }
    218 
    219 #pragma mark - RTCVideoRenderer
    220 
    221 // These methods may be called on non-main thread.
    222 - (void)setSize:(CGSize)size {
    223   __weak RTCEAGLVideoView* weakSelf = self;
    224   dispatch_async(dispatch_get_main_queue(), ^{
    225     RTCEAGLVideoView* strongSelf = weakSelf;
    226     [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
    227   });
    228 }
    229 
    230 - (void)renderFrame:(RTCI420Frame*)frame {
    231   self.i420Frame = frame;
    232 }
    233 
    234 #pragma mark - Private
    235 
    236 - (void)displayLinkTimerDidFire {
    237   // Don't render unless video frame have changed or the view content
    238   // has explicitly been marked dirty.
    239   if (!_isDirty && _glRenderer.lastDrawnFrame == self.i420Frame) {
    240     return;
    241   }
    242 
    243   // Always reset isDirty at this point, even if -[GLKView display]
    244   // won't be called in the case the drawable size is empty.
    245   _isDirty = NO;
    246 
    247   // Only call -[GLKView display] if the drawable size is
    248   // non-empty. Calling display will make the GLKView setup its
    249   // render buffer if necessary, but that will fail with error
    250   // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
    251   if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
    252     [_glkView display];
    253   }
    254 }
    255 
    256 - (void)setupGL {
    257   self.i420Frame = nil;
    258   [_glRenderer setupGL];
    259   _timer.isPaused = NO;
    260 }
    261 
    262 - (void)teardownGL {
    263   self.i420Frame = nil;
    264   _timer.isPaused = YES;
    265   [_glkView deleteDrawable];
    266   [_glRenderer teardownGL];
    267 }
    268 
    269 - (void)didBecomeActive {
    270   [self setupGL];
    271 }
    272 
    273 - (void)willResignActive {
    274   [self teardownGL];
    275 }
    276 
    277 @end
    278