| Index: webrtc/sdk/objc/Framework/Classes/UI/RTCEAGLVideoView.m
|
| diff --git a/webrtc/sdk/objc/Framework/Classes/UI/RTCEAGLVideoView.m b/webrtc/sdk/objc/Framework/Classes/UI/RTCEAGLVideoView.m
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..1fb03bc909e27305eee7eb41bd9f6066983f7e3e
|
| --- /dev/null
|
| +++ b/webrtc/sdk/objc/Framework/Classes/UI/RTCEAGLVideoView.m
|
| @@ -0,0 +1,297 @@
|
| +/*
|
| + * Copyright 2015 The WebRTC project authors. All Rights Reserved.
|
| + *
|
| + * Use of this source code is governed by a BSD-style license
|
| + * that can be found in the LICENSE file in the root of the source
|
| + * tree. An additional intellectual property rights grant can be found
|
| + * in the file PATENTS. All contributing project authors may
|
| + * be found in the AUTHORS file in the root of the source tree.
|
| + */
|
| +
|
| +#import "WebRTC/RTCEAGLVideoView.h"
|
| +
|
| +#import <GLKit/GLKit.h>
|
| +
|
| +#import "RTCShader+Private.h"
|
| +#import "WebRTC/RTCLogging.h"
|
| +#import "WebRTC/RTCVideoFrame.h"
|
| +
|
| +// RTCDisplayLinkTimer wraps a CADisplayLink and is set to fire every two screen
|
| +// refreshes, which should be 30fps. We wrap the display link in order to avoid
|
| +// a retain cycle since CADisplayLink takes a strong reference onto its target.
|
| +// The timer is paused by default.
|
| +@interface RTCDisplayLinkTimer : NSObject
|
| +
|
| +@property(nonatomic) BOOL isPaused;
|
| +
|
| +- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler;
|
| +- (void)invalidate;
|
| +
|
| +@end
|
| +
|
| +@implementation RTCDisplayLinkTimer {
|
| + CADisplayLink *_displayLink;
|
| + void (^_timerHandler)(void);
|
| +}
|
| +
|
| +- (instancetype)initWithTimerHandler:(void (^)(void))timerHandler {
|
| + NSParameterAssert(timerHandler);
|
| + if (self = [super init]) {
|
| + _timerHandler = timerHandler;
|
| + _displayLink =
|
| + [CADisplayLink displayLinkWithTarget:self
|
| + selector:@selector(displayLinkDidFire:)];
|
| + _displayLink.paused = YES;
|
| + // Set to half of screen refresh, which should be 30fps.
|
| + [_displayLink setFrameInterval:2];
|
| + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
|
| + forMode:NSRunLoopCommonModes];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + [self invalidate];
|
| +}
|
| +
|
| +- (BOOL)isPaused {
|
| + return _displayLink.paused;
|
| +}
|
| +
|
| +- (void)setIsPaused:(BOOL)isPaused {
|
| + _displayLink.paused = isPaused;
|
| +}
|
| +
|
| +- (void)invalidate {
|
| + [_displayLink invalidate];
|
| +}
|
| +
|
| +- (void)displayLinkDidFire:(CADisplayLink *)displayLink {
|
| + _timerHandler();
|
| +}
|
| +
|
| +@end
|
| +
|
| +// RTCEAGLVideoView wraps a GLKView which is setup with
|
| +// enableSetNeedsDisplay = NO for the purpose of gaining control of
|
| +// exactly when to call -[GLKView display]. This need for extra
|
| +// control is required to avoid triggering method calls on GLKView
|
| +// that results in attempting to bind the underlying render buffer
|
| +// when the drawable size would be empty which would result in the
|
| +// error GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. -[GLKView display] is
|
| +// the method that will trigger the binding of the render
|
| +// buffer. Because the standard behaviour of -[UIView setNeedsDisplay]
|
| +// is disabled for the reasons above, the RTCEAGLVideoView maintains
|
| +// its own |isDirty| flag.
|
| +
|
| +@interface RTCEAGLVideoView () <GLKViewDelegate>
|
| +// |videoFrame| is set when we receive a frame from a worker thread and is read
|
| +// from the display link callback so atomicity is required.
|
| +@property(atomic, strong) RTCVideoFrame *videoFrame;
|
| +@property(nonatomic, readonly) GLKView *glkView;
|
| +@end
|
| +
|
| +@implementation RTCEAGLVideoView {
|
| + RTCDisplayLinkTimer *_timer;
|
| + EAGLContext *_glContext;
|
| + // This flag should only be set and read on the main thread (e.g. by
|
| + // setNeedsDisplay)
|
| + BOOL _isDirty;
|
| + id<RTCShader> _i420Shader;
|
| + id<RTCShader> _nv12Shader;
|
| + RTCVideoFrame *_lastDrawnFrame;
|
| +}
|
| +
|
| +@synthesize delegate = _delegate;
|
| +@synthesize videoFrame = _videoFrame;
|
| +@synthesize glkView = _glkView;
|
| +
|
| +- (instancetype)initWithFrame:(CGRect)frame {
|
| + if (self = [super initWithFrame:frame]) {
|
| + [self configure];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
| + if (self = [super initWithCoder:aDecoder]) {
|
| + [self configure];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)configure {
|
| + EAGLContext *glContext =
|
| + [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
|
| + if (!glContext) {
|
| + glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
| + }
|
| + _glContext = glContext;
|
| +
|
| + // GLKView manages a framebuffer for us.
|
| + _glkView = [[GLKView alloc] initWithFrame:CGRectZero
|
| + context:_glContext];
|
| + _glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
|
| + _glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
|
| + _glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
|
| + _glkView.drawableMultisample = GLKViewDrawableMultisampleNone;
|
| + _glkView.delegate = self;
|
| + _glkView.layer.masksToBounds = YES;
|
| + _glkView.enableSetNeedsDisplay = NO;
|
| + [self addSubview:_glkView];
|
| +
|
| + // Listen to application state in order to clean up OpenGL before app goes
|
| + // away.
|
| + NSNotificationCenter *notificationCenter =
|
| + [NSNotificationCenter defaultCenter];
|
| + [notificationCenter addObserver:self
|
| + selector:@selector(willResignActive)
|
| + name:UIApplicationWillResignActiveNotification
|
| + object:nil];
|
| + [notificationCenter addObserver:self
|
| + selector:@selector(didBecomeActive)
|
| + name:UIApplicationDidBecomeActiveNotification
|
| + object:nil];
|
| +
|
| + // Frames are received on a separate thread, so we poll for current frame
|
| + // using a refresh rate proportional to screen refresh frequency. This
|
| + // occurs on the main thread.
|
| + __weak RTCEAGLVideoView *weakSelf = self;
|
| + _timer = [[RTCDisplayLinkTimer alloc] initWithTimerHandler:^{
|
| + RTCEAGLVideoView *strongSelf = weakSelf;
|
| + [strongSelf displayLinkTimerDidFire];
|
| + }];
|
| + [self setupGL];
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + [[NSNotificationCenter defaultCenter] removeObserver:self];
|
| + UIApplicationState appState =
|
| + [UIApplication sharedApplication].applicationState;
|
| + if (appState == UIApplicationStateActive) {
|
| + [self teardownGL];
|
| + }
|
| + [_timer invalidate];
|
| + if (_glContext && [EAGLContext currentContext] == _glContext) {
|
| + [EAGLContext setCurrentContext:nil];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - UIView
|
| +
|
| +- (void)setNeedsDisplay {
|
| + [super setNeedsDisplay];
|
| + _isDirty = YES;
|
| +}
|
| +
|
| +- (void)setNeedsDisplayInRect:(CGRect)rect {
|
| + [super setNeedsDisplayInRect:rect];
|
| + _isDirty = YES;
|
| +}
|
| +
|
| +- (void)layoutSubviews {
|
| + [super layoutSubviews];
|
| + _glkView.frame = self.bounds;
|
| +}
|
| +
|
| +#pragma mark - GLKViewDelegate
|
| +
|
| +// This method is called when the GLKView's content is dirty and needs to be
|
| +// redrawn. This occurs on main thread.
|
| +- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
|
| + // The renderer will draw the frame to the framebuffer corresponding to the
|
| + // one used by |view|.
|
| + RTCVideoFrame *frame = self.videoFrame;
|
| + if (!frame || frame == _lastDrawnFrame) {
|
| + return;
|
| + }
|
| + [self ensureGLContext];
|
| + glClear(GL_COLOR_BUFFER_BIT);
|
| + id<RTCShader> shader = nil;
|
| + if (frame.nativeHandle) {
|
| + if (!_nv12Shader) {
|
| + _nv12Shader = [[RTCNativeNV12Shader alloc] initWithContext:_glContext];
|
| + }
|
| + shader = _nv12Shader;
|
| + } else {
|
| + if (!_i420Shader) {
|
| + _i420Shader = [[RTCI420Shader alloc] initWithContext:_glContext];
|
| + }
|
| + shader = _i420Shader;
|
| + }
|
| + if (shader && [shader drawFrame:frame]) {
|
| + _lastDrawnFrame = frame;
|
| + } else {
|
| + RTCLog(@"Failed to draw frame.");
|
| + }
|
| +}
|
| +
|
| +#pragma mark - RTCVideoRenderer
|
| +
|
| +// These methods may be called on non-main thread.
|
| +- (void)setSize:(CGSize)size {
|
| + __weak RTCEAGLVideoView *weakSelf = self;
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + RTCEAGLVideoView *strongSelf = weakSelf;
|
| + [strongSelf.delegate videoView:strongSelf didChangeVideoSize:size];
|
| + });
|
| +}
|
| +
|
| +- (void)renderFrame:(RTCVideoFrame *)frame {
|
| + self.videoFrame = frame;
|
| +}
|
| +
|
| +#pragma mark - Private
|
| +
|
| +- (void)displayLinkTimerDidFire {
|
| + // Don't render unless video frame have changed or the view content
|
| + // has explicitly been marked dirty.
|
| + if (!_isDirty && _lastDrawnFrame == self.videoFrame) {
|
| + return;
|
| + }
|
| +
|
| + // Always reset isDirty at this point, even if -[GLKView display]
|
| + // won't be called in the case the drawable size is empty.
|
| + _isDirty = NO;
|
| +
|
| + // Only call -[GLKView display] if the drawable size is
|
| + // non-empty. Calling display will make the GLKView setup its
|
| + // render buffer if necessary, but that will fail with error
|
| + // GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT if size is empty.
|
| + if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
|
| + [_glkView display];
|
| + }
|
| +}
|
| +
|
| +- (void)setupGL {
|
| + self.videoFrame = nil;
|
| + [self ensureGLContext];
|
| + glDisable(GL_DITHER);
|
| + _timer.isPaused = NO;
|
| +}
|
| +
|
| +- (void)teardownGL {
|
| + self.videoFrame = nil;
|
| + _timer.isPaused = YES;
|
| + [_glkView deleteDrawable];
|
| + [self ensureGLContext];
|
| + _i420Shader = nil;
|
| + _nv12Shader = nil;
|
| +}
|
| +
|
| +- (void)didBecomeActive {
|
| + [self setupGL];
|
| +}
|
| +
|
| +- (void)willResignActive {
|
| + [self teardownGL];
|
| +}
|
| +
|
| +- (void)ensureGLContext {
|
| + NSAssert(_glContext, @"context shouldn't be nil");
|
| + if ([EAGLContext currentContext] != _glContext) {
|
| + [EAGLContext setCurrentContext:_glContext];
|
| + }
|
| +}
|
| +
|
| +@end
|
|
|