Add a UIView for rendering a video track.

RTCEAGLVideoView provides functionality to render a supplied RTCVideoTrack using OpenGLES2.

R=fischman@webrtc.org
BUG=3188

Review URL: https://webrtc-codereview.appspot.com/12489004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@6192 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/app/webrtc/objc/RTCEAGLVideoRenderer.mm b/talk/app/webrtc/objc/RTCEAGLVideoRenderer.mm
new file mode 100644
index 0000000..f5c6ead
--- /dev/null
+++ b/talk/app/webrtc/objc/RTCEAGLVideoRenderer.mm
@@ -0,0 +1,397 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "RTCEAGLVideoRenderer.h"
+
+#import <OpenGLES/ES2/gl.h>
+#import "RTCI420Frame.h"
+
+// TODO(tkchin): check and log openGL errors. Methods here return BOOLs in
+// anticipation of that happening in the future.
+
+// Convenience macro for writing shader code that converts a code snippet into
+// a C string during the C preprocessor step.
+#define RTC_STRINGIZE(...) #__VA_ARGS__
+
+// Vertex shader doesn't do anything except pass coordinates through.
+static const char kVertexShaderSource[] = RTC_STRINGIZE(
+  attribute vec2 position;
+  attribute vec2 texcoord;
+  varying vec2 v_texcoord;
+  void main() {
+    gl_Position = vec4(position.x, position.y, 0.0, 1.0);
+    v_texcoord = texcoord;
+  }
+);
+
+// Fragment shader converts YUV values from input textures into a final RGB
+// pixel. The conversion formula is from http://www.fourcc.org/fccyvrgb.php.
+static const char kFragmentShaderSource[] = RTC_STRINGIZE(
+  precision highp float;
+  varying vec2 v_texcoord;
+  uniform lowp sampler2D s_textureY;
+  uniform lowp sampler2D s_textureU;
+  uniform lowp sampler2D s_textureV;
+  void main() {
+    float y, u, v, r, g, b;
+    y = texture2D(s_textureY, v_texcoord).r;
+    u = texture2D(s_textureU, v_texcoord).r;
+    v = texture2D(s_textureV, v_texcoord).r;
+    u = u - 0.5;
+    v = v - 0.5;
+    r = y + 1.403 * v;
+    g = y - 0.344 * u - 0.714 * v;
+    b = y + 1.770 * u;
+    gl_FragColor = vec4(r, g, b, 1.0);
+  }
+);
+
+// Compiles a shader of the given |type| with GLSL source |source| and returns
+// the shader handle or 0 on error.
+GLuint CreateShader(GLenum type, const GLchar* source) {
+  GLuint shader = glCreateShader(type);
+  if (!shader) {
+    return 0;
+  }
+  glShaderSource(shader, 1, &source, NULL);
+  glCompileShader(shader);
+  GLint compileStatus = GL_FALSE;
+  glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
+  if (compileStatus == GL_FALSE) {
+    glDeleteShader(shader);
+    shader = 0;
+  }
+  return shader;
+}
+
+// Links a shader program with the given vertex and fragment shaders and
+// returns the program handle or 0 on error.
+GLuint CreateProgram(GLuint vertexShader, GLuint fragmentShader) {
+  if (vertexShader == 0 || fragmentShader == 0) {
+    return 0;
+  }
+  GLuint program = glCreateProgram();
+  if (!program) {
+    return 0;
+  }
+  glAttachShader(program, vertexShader);
+  glAttachShader(program, fragmentShader);
+  glLinkProgram(program);
+  GLint linkStatus = GL_FALSE;
+  glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+  if (linkStatus == GL_FALSE) {
+    glDeleteProgram(program);
+    program = 0;
+  }
+  return program;
+}
+
+// When modelview and projection matrices are identity (default) the world is
+// contained in the square around origin with unit size 2. Drawing to these
+// coordinates is equivalent to drawing to the entire screen. The texture is
+// stretched over that square using texture coordinates (u, v) that range
+// from (0, 0) to (1, 1) inclusive. Texture coordinates are flipped vertically
+// here because the incoming frame has origin in upper left hand corner but
+// OpenGL expects origin in bottom left corner.
+const GLfloat gVertices[] = {
+    // X, Y, U, V.
+    -1, -1, 0, 1,  // Bottom left.
+    1,  -1, 1, 1,  // Bottom right.
+    1,  1,  1, 0,  // Top right.
+    -1, 1,  0, 0,  // Top left.
+};
+
+// |kNumTextures| must not exceed 8, which is the limit in OpenGLES2. Two sets
+// of 3 textures are used here, one for each of the Y, U and V planes. Having
+// two sets alleviates CPU blockage in the event that the GPU is asked to render
+// to a texture that is already in use.
+static const GLsizei kNumTextureSets = 2;
+static const GLsizei kNumTextures = 3 * kNumTextureSets;
+
+@implementation RTCEAGLVideoRenderer {
+  EAGLContext* _context;
+  BOOL _isInitialized;
+  NSUInteger _currentTextureSet;
+  // Handles for OpenGL constructs.
+  GLuint _textures[kNumTextures];
+  GLuint _program;
+  GLuint _vertexBuffer;
+  GLint _position;
+  GLint _texcoord;
+  GLint _ySampler;
+  GLint _uSampler;
+  GLint _vSampler;
+}
+
++ (void)initialize {
+  // Disable dithering for performance.
+  glDisable(GL_DITHER);
+}
+
+- (instancetype)initWithContext:(EAGLContext*)context {
+  NSAssert(context != nil, @"context cannot be nil");
+  if (self = [super init]) {
+    _context = context;
+  }
+  return self;
+}
+
+- (BOOL)drawFrame:(RTCI420Frame*)frame {
+  if (!_isInitialized) {
+    return NO;
+  }
+  if (_lastDrawnFrame == frame) {
+    return NO;
+  }
+  [self ensureGLContext];
+  if (![self updateTextureSizesForFrame:frame] ||
+      ![self updateTextureDataForFrame:frame]) {
+    return NO;
+  }
+  glClear(GL_COLOR_BUFFER_BIT);
+  glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
+  glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+  _lastDrawnFrame = frame;
+  return YES;
+}
+
+- (void)setupGL {
+  if (_isInitialized) {
+    return;
+  }
+  [self ensureGLContext];
+  if (![self setupProgram]) {
+    return;
+  }
+  if (![self setupTextures]) {
+    return;
+  }
+  if (![self setupVertices]) {
+    return;
+  }
+  glUseProgram(_program);
+  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+  glClearColor(0, 0, 0, 1);
+  _isInitialized = YES;
+}
+
+- (void)teardownGL {
+  if (!_isInitialized) {
+    return;
+  }
+  [self ensureGLContext];
+  glDeleteProgram(_program);
+  _program = 0;
+  glDeleteTextures(kNumTextures, _textures);
+  glDeleteBuffers(1, &_vertexBuffer);
+  _vertexBuffer = 0;
+  _isInitialized = NO;
+}
+
+#pragma mark - Private
+
+- (void)ensureGLContext {
+  if ([EAGLContext currentContext] != _context) {
+    NSAssert(_context, @"context shouldn't be nil");
+    [EAGLContext setCurrentContext:_context];
+  }
+}
+
+- (BOOL)setupProgram {
+  NSAssert(!_program, @"program already set up");
+  GLuint vertexShader = CreateShader(GL_VERTEX_SHADER, kVertexShaderSource);
+  GLuint fragmentShader =
+      CreateShader(GL_FRAGMENT_SHADER, kFragmentShaderSource);
+  _program = CreateProgram(vertexShader, fragmentShader);
+  // Shaders are created only to generate program.
+  if (vertexShader) {
+    glDeleteShader(vertexShader);
+  }
+  if (fragmentShader) {
+    glDeleteShader(fragmentShader);
+  }
+  if (!_program) {
+    return NO;
+  }
+  _position = glGetAttribLocation(_program, "position");
+  _texcoord = glGetAttribLocation(_program, "texcoord");
+  _ySampler = glGetUniformLocation(_program, "s_textureY");
+  _uSampler = glGetUniformLocation(_program, "s_textureU");
+  _vSampler = glGetUniformLocation(_program, "s_textureV");
+  if (_position < 0 || _texcoord < 0 || _ySampler < 0 || _uSampler < 0 ||
+      _vSampler < 0) {
+    return NO;
+  }
+  return YES;
+}
+
+- (BOOL)setupTextures {
+  glGenTextures(kNumTextures, _textures);
+  // Set parameters for each of the textures we created.
+  for (GLsizei i = 0; i < kNumTextures; i++) {
+    glActiveTexture(GL_TEXTURE0 + i);
+    glBindTexture(GL_TEXTURE_2D, _textures[i]);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+  }
+  return YES;
+}
+
+- (BOOL)updateTextureSizesForFrame:(RTCI420Frame*)frame {
+  if (frame.height == _lastDrawnFrame.height &&
+      frame.width == _lastDrawnFrame.width &&
+      frame.chromaWidth == _lastDrawnFrame.chromaWidth &&
+      frame.chromaHeight == _lastDrawnFrame.chromaHeight) {
+    return YES;
+  }
+  GLsizei lumaWidth = frame.width;
+  GLsizei lumaHeight = frame.height;
+  GLsizei chromaWidth = frame.chromaWidth;
+  GLsizei chromaHeight = frame.chromaHeight;
+  for (GLint i = 0; i < kNumTextureSets; i++) {
+    glActiveTexture(GL_TEXTURE0 + i * 3);
+    glTexImage2D(GL_TEXTURE_2D,
+                 0,
+                 GL_LUMINANCE,
+                 lumaWidth,
+                 lumaHeight,
+                 0,
+                 GL_LUMINANCE,
+                 GL_UNSIGNED_BYTE,
+                 0);
+
+    glActiveTexture(GL_TEXTURE0 + i * 3 + 1);
+    glTexImage2D(GL_TEXTURE_2D,
+                 0,
+                 GL_LUMINANCE,
+                 chromaWidth,
+                 chromaHeight,
+                 0,
+                 GL_LUMINANCE,
+                 GL_UNSIGNED_BYTE,
+                 0);
+
+    glActiveTexture(GL_TEXTURE0 + i * 3 + 2);
+    glTexImage2D(GL_TEXTURE_2D,
+                 0,
+                 GL_LUMINANCE,
+                 chromaWidth,
+                 chromaHeight,
+                 0,
+                 GL_LUMINANCE,
+                 GL_UNSIGNED_BYTE,
+                 0);
+  }
+  return YES;
+}
+
+- (BOOL)updateTextureDataForFrame:(RTCI420Frame*)frame {
+  NSUInteger textureOffset = _currentTextureSet * 3;
+  NSAssert(textureOffset + 3 <= kNumTextures, @"invalid offset");
+  NSParameterAssert(frame.yPitch == frame.width);
+  NSParameterAssert(frame.uPitch == frame.chromaWidth);
+  NSParameterAssert(frame.vPitch == frame.chromaWidth);
+
+  glActiveTexture(GL_TEXTURE0 + textureOffset);
+  // When setting texture sampler uniforms, the texture index is used not
+  // the texture handle.
+  glUniform1i(_ySampler, textureOffset);
+  glTexImage2D(GL_TEXTURE_2D,
+               0,
+               GL_LUMINANCE,
+               frame.width,
+               frame.height,
+               0,
+               GL_LUMINANCE,
+               GL_UNSIGNED_BYTE,
+               frame.yPlane);
+
+  glActiveTexture(GL_TEXTURE0 + textureOffset + 1);
+  glUniform1i(_uSampler, textureOffset + 1);
+  glTexImage2D(GL_TEXTURE_2D,
+               0,
+               GL_LUMINANCE,
+               frame.chromaWidth,
+               frame.chromaHeight,
+               0,
+               GL_LUMINANCE,
+               GL_UNSIGNED_BYTE,
+               frame.uPlane);
+
+  glActiveTexture(GL_TEXTURE0 + textureOffset + 2);
+  glUniform1i(_vSampler, textureOffset + 2);
+  glTexImage2D(GL_TEXTURE_2D,
+               0,
+               GL_LUMINANCE,
+               frame.chromaWidth,
+               frame.chromaHeight,
+               0,
+               GL_LUMINANCE,
+               GL_UNSIGNED_BYTE,
+               frame.vPlane);
+
+  _currentTextureSet = (_currentTextureSet + 1) % kNumTextureSets;
+  return YES;
+}
+
+- (BOOL)setupVertices {
+  NSAssert(!_vertexBuffer, @"vertex buffer already set up");
+  glGenBuffers(1, &_vertexBuffer);
+  if (!_vertexBuffer) {
+    return NO;
+  }
+  glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
+  glBufferData(GL_ARRAY_BUFFER, sizeof(gVertices), gVertices, GL_DYNAMIC_DRAW);
+
+  // Read position attribute from |gVertices| with size of 2 and stride of 4
+  // beginning at the start of the array. The last argument indicates offset
+  // of data within |gVertices| as supplied to the vertex buffer.
+  glVertexAttribPointer(
+      _position, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)0);
+  glEnableVertexAttribArray(_position);
+
+  // Read texcoord attribute from |gVertices| with size of 2 and stride of 4
+  // beginning at the first texcoord in the array. The last argument indicates
+  // offset of data within |gVertices| as supplied to the vertex buffer.
+  glVertexAttribPointer(_texcoord,
+                        2,
+                        GL_FLOAT,
+                        GL_FALSE,
+                        4 * sizeof(GLfloat),
+                        (void*)(2 * sizeof(GLfloat)));
+  glEnableVertexAttribArray(_texcoord);
+
+  return YES;
+}
+
+@end
diff --git a/talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h b/talk/app/webrtc/objc/RTCEAGLVideoView+Internal.h
similarity index 75%
rename from talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h
rename to talk/app/webrtc/objc/RTCEAGLVideoView+Internal.h
index af72bde..10df2e3 100644
--- a/talk/app/webrtc/objc/public/RTCVideoRendererDelegate.h
+++ b/talk/app/webrtc/objc/RTCEAGLVideoView+Internal.h
@@ -1,6 +1,6 @@
 /*
  * libjingle
- * Copyright 2013, Google Inc.
+ * Copyright 2014, Google Inc.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are met:
@@ -27,18 +27,10 @@
 
 #import <Foundation/Foundation.h>
 
-@class RTCI420Frame;
-@class RTCVideoRenderer;
+#import "RTCEAGLVideoView.h"
+#import "RTCVideoRenderer.h"
 
-// RTCVideoRendererDelegate is a protocol for an object that must be
-// implemented to get messages when rendering.
-@protocol RTCVideoRendererDelegate<NSObject>
-
-// The size of the frame.
-- (void)videoRenderer:(RTCVideoRenderer *)videoRenderer setSize:(CGSize)size;
-
-// The frame to be displayed.
-- (void)videoRenderer:(RTCVideoRenderer *)videoRenderer
-          renderFrame:(RTCI420Frame *)frame;
-
+// TODO(tkchin): Move declaration to implementation file. Exposed here in order
+// to support deprecated methods in RTCVideoRenderer.
+@interface RTCEAGLVideoView (Internal) <RTCVideoRendererDelegate>
 @end
diff --git a/talk/app/webrtc/objc/RTCEAGLVideoView.m b/talk/app/webrtc/objc/RTCEAGLVideoView.m
new file mode 100644
index 0000000..b970325
--- /dev/null
+++ b/talk/app/webrtc/objc/RTCEAGLVideoView.m
@@ -0,0 +1,186 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "RTCEAGLVideoView+Internal.h"
+
+#import <GLKit/GLKit.h>
+#import <QuartzCore/QuartzCore.h>
+
+#import "RTCEAGLVideoRenderer.h"
+#import "RTCVideoRenderer.h"
+#import "RTCVideoTrack.h"
+
+@interface RTCEAGLVideoView () <GLKViewDelegate>
+@property(atomic, strong) RTCI420Frame* i420Frame;
+@end
+
+@implementation RTCEAGLVideoView {
+  CADisplayLink* _displayLink;
+  GLKView* _glkView;
+  RTCEAGLVideoRenderer* _glRenderer;
+  RTCVideoRenderer* _videoRenderer;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    EAGLContext* glContext =
+        [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+    _glRenderer = [[RTCEAGLVideoRenderer alloc] initWithContext: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;
+    [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];
+    _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];
+    _videoRenderer = [[RTCVideoRenderer alloc] initWithDelegate:self];
+    [self setupGL];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+  UIApplicationState appState =
+      [UIApplication sharedApplication].applicationState;
+  if (appState == UIApplicationStateActive) {
+    [self teardownGL];
+  }
+}
+
+- (void)setVideoTrack:(RTCVideoTrack*)videoTrack {
+  if (_videoTrack == videoTrack) {
+    return;
+  }
+  [_videoTrack removeRenderer:_videoRenderer];
+  _videoTrack = videoTrack;
+  [_videoTrack addRenderer:_videoRenderer];
+  // TODO(tkchin): potentially handle changes in track state - e.g. render
+  // black if track fails.
+}
+
+#pragma mark - UIView
+
+- (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 {
+  if (self.i420Frame) {
+    // The renderer will draw the frame to the framebuffer corresponding to the
+    // one used by |view|.
+    [_glRenderer drawFrame:self.i420Frame];
+  }
+}
+
+#pragma mark - Private
+
+// 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 main thread.
+- (void)displayLinkDidFire:(CADisplayLink*)displayLink {
+  // Don't render if frame hasn't changed.
+  if (_glRenderer.lastDrawnFrame == self.i420Frame) {
+    return;
+  }
+  // This tells the GLKView that it's dirty, which will then call the the
+  // GLKViewDelegate method implemented above.
+  [_glkView setNeedsDisplay];
+}
+
+- (void)setupGL {
+  [_glRenderer setupGL];
+  _displayLink.paused = NO;
+}
+
+- (void)teardownGL {
+  _displayLink.paused = YES;
+  [_glkView deleteDrawable];
+  [_glRenderer teardownGL];
+}
+
+- (void)didBecomeActive {
+  [self setupGL];
+}
+
+- (void)willResignActive {
+  [self teardownGL];
+}
+
+@end
+
+@implementation RTCEAGLVideoView (Internal)
+
+#pragma mark - RTCVideoRendererDelegate
+
+// These methods are called when the video track has frame information to
+// provide. This occurs on non-main thread.
+- (void)renderer:(RTCVideoRenderer*)renderer
+      didSetSize:(CGSize)size {
+  // Size is checked in renderer as frames arrive, no need to do anything here.
+}
+
+- (void)renderer:(RTCVideoRenderer*)renderer
+    didReceiveFrame:(RTCI420Frame*)frame {
+  self.i420Frame = frame;
+}
+
+@end
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.h b/talk/app/webrtc/objc/RTCI420Frame+Internal.h
similarity index 73%
copy from talk/examples/ios/AppRTCDemo/APPRTCVideoView.h
copy to talk/app/webrtc/objc/RTCI420Frame+Internal.h
index 238798e..622c0b3 100644
--- a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.h
+++ b/talk/app/webrtc/objc/RTCI420Frame+Internal.h
@@ -1,6 +1,6 @@
 /*
  * libjingle
- * Copyright 2013, Google Inc.
+ * Copyright 2014, Google Inc.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are met:
@@ -25,19 +25,12 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import <UIKit/UIKit.h>
+#import "RTCI420Frame.h"
 
-@class RTCVideoTrack;
+#include "talk/media/base/videoframe.h"
 
-// This class encapsulates VideoRenderIosView.
-@interface APPRTCVideoView : UIView
+@interface RTCI420Frame (Internal)
 
-// Property to get/set required video orientation.
-@property(nonatomic, assign) UIInterfaceOrientation videoOrientation;
-// Specifies whether the object represents a local or remote video stream.
-@property(nonatomic, assign) BOOL isRemote;
-
-// Sets up the underlying renderer and track objects.
-- (void)renderVideoTrackInterface:(RTCVideoTrack*)track;
+- (instancetype)initWithVideoFrame:(const cricket::VideoFrame*)videoFrame;
 
 @end
diff --git a/talk/app/webrtc/objc/RTCI420Frame.mm b/talk/app/webrtc/objc/RTCI420Frame.mm
index df84fc1..eff3102 100644
--- a/talk/app/webrtc/objc/RTCI420Frame.mm
+++ b/talk/app/webrtc/objc/RTCI420Frame.mm
@@ -27,8 +27,68 @@
 
 #import "RTCI420Frame.h"
 
-@implementation RTCI420Frame
+#include "talk/base/scoped_ptr.h"
+#include "talk/media/base/videoframe.h"
 
-// TODO(hughv): Should this just be a cricket::VideoFrame wrapper object?
+@implementation RTCI420Frame {
+  talk_base::scoped_ptr<cricket::VideoFrame> _videoFrame;
+}
+
+- (NSUInteger)width {
+  return _videoFrame->GetWidth();
+}
+
+- (NSUInteger)height {
+  return _videoFrame->GetHeight();
+}
+
+- (NSUInteger)chromaWidth {
+  return _videoFrame->GetChromaWidth();
+}
+
+- (NSUInteger)chromaHeight {
+  return _videoFrame->GetChromaHeight();
+}
+
+- (NSUInteger)chromaSize {
+  return _videoFrame->GetChromaSize();
+}
+
+- (const uint8_t*)yPlane {
+  return _videoFrame->GetYPlane();
+}
+
+- (const uint8_t*)uPlane {
+  return _videoFrame->GetUPlane();
+}
+
+- (const uint8_t*)vPlane {
+  return _videoFrame->GetVPlane();
+}
+
+- (NSInteger)yPitch {
+  return _videoFrame->GetYPitch();
+}
+
+- (NSInteger)uPitch {
+  return _videoFrame->GetUPitch();
+}
+
+- (NSInteger)vPitch {
+  return _videoFrame->GetVPitch();
+}
+
+@end
+
+@implementation RTCI420Frame (Internal)
+
+- (instancetype)initWithVideoFrame:(cricket::VideoFrame*)videoFrame {
+  if (self = [super init]) {
+    // Keep a shallow copy of the video frame. The underlying frame buffer is
+    // not copied.
+    _videoFrame.reset(videoFrame->Copy());
+  }
+  return self;
+}
 
 @end
diff --git a/talk/app/webrtc/objc/RTCMediaStreamTrack.mm b/talk/app/webrtc/objc/RTCMediaStreamTrack.mm
index 0c7fc5c..5931312 100644
--- a/talk/app/webrtc/objc/RTCMediaStreamTrack.mm
+++ b/talk/app/webrtc/objc/RTCMediaStreamTrack.mm
@@ -32,8 +32,24 @@
 #import "RTCMediaStreamTrack+Internal.h"
 #import "RTCEnumConverter.h"
 
+namespace webrtc {
+
+class RTCMediaStreamTrackObserver : public ObserverInterface {
+ public:
+  RTCMediaStreamTrackObserver(RTCMediaStreamTrack* track) { _track = track; }
+
+  virtual void OnChanged() OVERRIDE {
+    [_track.delegate mediaStreamTrackDidChange:_track];
+  }
+
+ private:
+  __weak RTCMediaStreamTrack* _track;
+};
+}
+
 @implementation RTCMediaStreamTrack {
   talk_base::scoped_refptr<webrtc::MediaStreamTrackInterface> _mediaTrack;
+  talk_base::scoped_ptr<webrtc::RTCMediaStreamTrackObserver> _observer;
 }
 
 @synthesize label;
@@ -91,13 +107,19 @@
     self = nil;
     return nil;
   }
-  if ((self = [super init])) {
+  if (self = [super init]) {
     _mediaTrack = mediaTrack;
     label = @(mediaTrack->id().c_str());
+    _observer.reset(new webrtc::RTCMediaStreamTrackObserver(self));
+    _mediaTrack->RegisterObserver(_observer.get());
   }
   return self;
 }
 
+- (void)dealloc {
+  _mediaTrack->UnregisterObserver(_observer.get());
+}
+
 - (talk_base::scoped_refptr<webrtc::MediaStreamTrackInterface>)mediaTrack {
   return _mediaTrack;
 }
diff --git a/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h b/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h
index 6672cfa..22e445c 100644
--- a/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h
+++ b/talk/app/webrtc/objc/RTCVideoRenderer+Internal.h
@@ -31,8 +31,6 @@
 
 @interface RTCVideoRenderer (Internal)
 
-// TODO(hughv): Use smart pointer.
-@property(nonatomic, assign, readonly)
-    webrtc::VideoRendererInterface *videoRenderer;
+@property(nonatomic, readonly) webrtc::VideoRendererInterface* videoRenderer;
 
 @end
diff --git a/talk/app/webrtc/objc/RTCVideoRenderer.mm b/talk/app/webrtc/objc/RTCVideoRenderer.mm
index be3d205..0704181 100644
--- a/talk/app/webrtc/objc/RTCVideoRenderer.mm
+++ b/talk/app/webrtc/objc/RTCVideoRenderer.mm
@@ -32,173 +32,71 @@
 #import "RTCVideoRenderer+Internal.h"
 
 #if TARGET_OS_IPHONE
-#import <UIKit/UIKit.h>
+#import "RTCEAGLVideoView+Internal.h"
+#endif
+#import "RTCI420Frame+Internal.h"
 
-#import "RTCI420Frame.h"
-#import "RTCVideoRendererDelegate.h"
+namespace webrtc {
 
-#import "webrtc/modules/video_render/ios/video_render_ios_impl.h"
-#import "webrtc/modules/video_render/ios/video_render_ios_view.h"
-
-#include "common_video/interface/i420_video_frame.h"
-#include "talk/app/webrtc/mediastreaminterface.h"
-#include "talk/media/base/videoframe.h"
-#include "webrtc/modules/video_render/include/video_render_defines.h"
-
-// An adapter presenting VideoRendererInterface's API and delegating to
-// a VideoRenderCallback.  Suitable for feeding to
-// VideoTrackInterface::AddRenderer().
-class CallbackConverter : public webrtc::VideoRendererInterface {
+class RTCVideoRendererAdapter : public VideoRendererInterface {
  public:
-  CallbackConverter(webrtc::VideoRenderCallback* callback,
-                    const uint32_t streamId)
-      : callback_(callback), streamId_(streamId) {}
+  RTCVideoRendererAdapter(RTCVideoRenderer* renderer) { _renderer = renderer; }
 
-  virtual void SetSize(int width, int height) {};
-  virtual void RenderFrame(const cricket::VideoFrame* frame) {
-    // Make this into an I420VideoFrame.
-    size_t width = frame->GetWidth();
-    size_t height = frame->GetHeight();
+  virtual void SetSize(int width, int height) OVERRIDE {
+    [_renderer.delegate renderer:_renderer
+                      didSetSize:CGSizeMake(width, height)];
+  }
 
-    size_t y_plane_size = width * height;
-    size_t uv_plane_size = frame->GetChromaSize();
-
-    webrtc::I420VideoFrame i420Frame;
-    i420Frame.CreateFrame(y_plane_size,
-                          frame->GetYPlane(),
-                          uv_plane_size,
-                          frame->GetUPlane(),
-                          uv_plane_size,
-                          frame->GetVPlane(),
-                          width,
-                          height,
-                          frame->GetYPitch(),
-                          frame->GetUPitch(),
-                          frame->GetVPitch());
-
-    i420Frame.set_render_time_ms(frame->GetTimeStamp() / 1000000);
-
-    callback_->RenderFrame(streamId_, i420Frame);
+  virtual void RenderFrame(const cricket::VideoFrame* frame) OVERRIDE {
+    if (!_renderer.delegate) {
+      return;
+    }
+    RTCI420Frame* i420Frame = [[RTCI420Frame alloc] initWithVideoFrame:frame];
+    [_renderer.delegate renderer:_renderer didReceiveFrame:i420Frame];
   }
 
  private:
-  webrtc::VideoRenderCallback* callback_;
-  const uint32_t streamId_;
+  __weak RTCVideoRenderer* _renderer;
 };
+}
 
 @implementation RTCVideoRenderer {
-  VideoRenderIosView* _renderView;
-  UIActivityIndicatorView* _activityIndicator;
-  CallbackConverter* _converter;
-  talk_base::scoped_ptr<webrtc::VideoRenderIosImpl> _iosRenderer;
+  talk_base::scoped_ptr<webrtc::RTCVideoRendererAdapter> _adapter;
+#if TARGET_OS_IPHONE
+  RTCEAGLVideoView* _videoView;
+#endif
 }
 
-@synthesize delegate = _delegate;
-
-- (id)initWithDelegate:(id<RTCVideoRendererDelegate>)delegate {
-  // TODO(hughv): Create video renderer.
-  [self doesNotRecognizeSelector:_cmd];
-  return self;
-}
-
-- (id)initWithView:(UIView*)view {
-  if ((self = [super init])) {
-    CGRect frame =
-        CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height);
-    _renderView = [[VideoRenderIosView alloc] initWithFrame:frame];
-    _iosRenderer.reset(
-        new webrtc::VideoRenderIosImpl(0, (__bridge void*)_renderView, NO));
-    if (_iosRenderer->Init() == -1) {
-      self = nil;
-    } else {
-      webrtc::VideoRenderCallback* callback =
-          _iosRenderer->AddIncomingRenderStream(0, 1, 0, 0, 1, 1);
-      _converter = new CallbackConverter(callback, 0);
-      _iosRenderer->StartRender();
-      [view addSubview:_renderView];
-      _renderView.autoresizingMask =
-          UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
-      _renderView.translatesAutoresizingMaskIntoConstraints = YES;
-
-      _activityIndicator = [[UIActivityIndicatorView alloc]
-          initWithActivityIndicatorStyle:
-              UIActivityIndicatorViewStyleWhiteLarge];
-      _activityIndicator.frame = view.bounds;
-      _activityIndicator.hidesWhenStopped = YES;
-      [view addSubview:_activityIndicator];
-      _activityIndicator.autoresizingMask =
-          UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
-      _activityIndicator.translatesAutoresizingMaskIntoConstraints = YES;
-      [_activityIndicator startAnimating];
-    }
-  }
-  return self;
-}
-
-- (void)start {
-  [_activityIndicator stopAnimating];
-  [_activityIndicator removeFromSuperview];
-  _iosRenderer->StartRender();
-}
-
-- (void)stop {
-  [_activityIndicator stopAnimating];
-  [_activityIndicator removeFromSuperview];
-  _iosRenderer->StopRender();
-}
-
-@end
-
-@implementation RTCVideoRenderer (Internal)
-
-- (webrtc::VideoRendererInterface*)videoRenderer {
-  return _converter;
-}
-
-@end
-
-#else  // TARGET_OS_IPHONE
-
-// TODO(fischman): implement an OS/X RTCVideoRenderer (and add to
-// RTCPeerConnectionTest!).
-
-#import "RTCI420Frame.h"
-#import "RTCVideoRendererDelegate.h"
-@implementation RTCVideoRenderer
-@synthesize delegate = _delegate;
-+ (RTCVideoRenderer*)videoRendererWithFrame:(CGRect)frame {
-  // TODO(hughv): Implement.
-  return nil;
-}
-- (id)initWithDelegate:(id<RTCVideoRendererDelegate>)delegate {
-  if ((self = [super init])) {
+- (instancetype)initWithDelegate:(id<RTCVideoRendererDelegate>)delegate {
+  if (self = [super init]) {
     _delegate = delegate;
-    // TODO(hughv): Create video renderer.
+    _adapter.reset(new webrtc::RTCVideoRendererAdapter(self));
   }
   return self;
 }
-- (id)initWithView:(UIView*)view {
-  return nil;
+
+#if TARGET_OS_IPHONE
+// TODO(tkchin): remove shim for deprecated method.
+- (instancetype)initWithView:(UIView*)view {
+  if (self = [super init]) {
+    _videoView = [[RTCEAGLVideoView alloc] initWithFrame:view.bounds];
+    _videoView.autoresizingMask =
+        UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
+    _videoView.translatesAutoresizingMaskIntoConstraints = YES;
+    [view addSubview:_videoView];
+    self.delegate = _videoView;
+    _adapter.reset(new webrtc::RTCVideoRendererAdapter(self));
+  }
+  return self;
 }
-- (void)setTransform:(CGAffineTransform)transform {
-}
-- (void)start {
-}
-- (void)stop {
-}
+#endif
 
 @end
+
 @implementation RTCVideoRenderer (Internal)
-- (id)initWithVideoRenderer:(webrtc::VideoRendererInterface*)videoRenderer {
-  if ((self = [super init])) {
-    // TODO(hughv): Implement.
-  }
-  return self;
-}
-- (webrtc::VideoRendererInterface*)videoRenderer {
-  // TODO(hughv): Implement.
-  return NULL;
-}
-@end
 
-#endif  // TARGET_OS_IPHONE
+- (webrtc::VideoRendererInterface*)videoRenderer {
+  return _adapter.get();
+}
+
+@end
diff --git a/talk/app/webrtc/objc/public/RTCEAGLVideoRenderer.h b/talk/app/webrtc/objc/public/RTCEAGLVideoRenderer.h
new file mode 100644
index 0000000..d9cdccc
--- /dev/null
+++ b/talk/app/webrtc/objc/public/RTCEAGLVideoRenderer.h
@@ -0,0 +1,65 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+#import <GLKit/GLKit.h>
+
+@class RTCI420Frame;
+
+// RTCEAGLVideoRenderer issues appropriate EAGL commands to draw a frame to the
+// currently bound framebuffer. OpenGL framebuffer creation and management
+// should be handled elsewhere using the same context used to initialize this
+// class.
+@interface RTCEAGLVideoRenderer : NSObject
+
+// The last successfully drawn frame. Used to avoid drawing frames unnecessarily
+// hence saving battery life by reducing load.
+@property(nonatomic, readonly) RTCI420Frame* lastDrawnFrame;
+
+- (instancetype)initWithContext:(EAGLContext*)context;
+
+// Draws |frame| onto the currently bound OpenGL framebuffer. |setupGL| must be
+// called before this function will succeed.
+- (BOOL)drawFrame:(RTCI420Frame*)frame;
+
+// The following methods are used to manage OpenGL resources. On iOS
+// applications should release resources when placed in background for use in
+// the foreground application. In fact, attempting to call OpenGLES commands
+// while in background will result in application termination.
+
+// Sets up the OpenGL state needed for rendering.
+- (void)setupGL;
+// Tears down the OpenGL state created by |setupGL|.
+- (void)teardownGL;
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+// Disallow init and don't add to documentation
+- (id)init __attribute__((
+    unavailable("init is not a supported initializer for this class.")));
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
+
+@end
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.h b/talk/app/webrtc/objc/public/RTCEAGLVideoView.h
similarity index 75%
rename from talk/examples/ios/AppRTCDemo/APPRTCVideoView.h
rename to talk/app/webrtc/objc/public/RTCEAGLVideoView.h
index 238798e..e49cc58 100644
--- a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.h
+++ b/talk/app/webrtc/objc/public/RTCEAGLVideoView.h
@@ -1,6 +1,6 @@
 /*
  * libjingle
- * Copyright 2013, Google Inc.
+ * Copyright 2014, Google Inc.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are met:
@@ -25,19 +25,15 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#import <Foundation/Foundation.h>
 #import <UIKit/UIKit.h>
 
+#import "RTCVideoRenderer.h"
+
 @class RTCVideoTrack;
+// RTCEAGLVideoView renders |videoTrack| onto itself using OpenGLES.
+@interface RTCEAGLVideoView : UIView
 
-// This class encapsulates VideoRenderIosView.
-@interface APPRTCVideoView : UIView
-
-// Property to get/set required video orientation.
-@property(nonatomic, assign) UIInterfaceOrientation videoOrientation;
-// Specifies whether the object represents a local or remote video stream.
-@property(nonatomic, assign) BOOL isRemote;
-
-// Sets up the underlying renderer and track objects.
-- (void)renderVideoTrackInterface:(RTCVideoTrack*)track;
+@property(nonatomic, strong) RTCVideoTrack* videoTrack;
 
 @end
diff --git a/talk/app/webrtc/objc/public/RTCI420Frame.h b/talk/app/webrtc/objc/public/RTCI420Frame.h
index bf58085..737968c 100644
--- a/talk/app/webrtc/objc/public/RTCI420Frame.h
+++ b/talk/app/webrtc/objc/public/RTCI420Frame.h
@@ -30,7 +30,24 @@
 // RTCI420Frame is an ObjectiveC version of cricket::VideoFrame.
 @interface RTCI420Frame : NSObject
 
-// TODO(hughv): Implement this when iOS VP8 is ready.
+@property(nonatomic, readonly) NSUInteger width;
+@property(nonatomic, readonly) NSUInteger height;
+@property(nonatomic, readonly) NSUInteger chromaWidth;
+@property(nonatomic, readonly) NSUInteger chromaHeight;
+@property(nonatomic, readonly) NSUInteger chromaSize;
+// These can return NULL if the object is not backed by a buffer.
+@property(nonatomic, readonly) const uint8_t* yPlane;
+@property(nonatomic, readonly) const uint8_t* uPlane;
+@property(nonatomic, readonly) const uint8_t* vPlane;
+@property(nonatomic, readonly) NSInteger yPitch;
+@property(nonatomic, readonly) NSInteger uPitch;
+@property(nonatomic, readonly) NSInteger vPitch;
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+// Disallow init and don't add to documentation
+- (id)init __attribute__((
+    unavailable("init is not a supported initializer for this class.")));
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
 
 @end
 
diff --git a/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h b/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h
index f8f9369..1b9339d 100644
--- a/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h
+++ b/talk/app/webrtc/objc/public/RTCMediaStreamTrack.h
@@ -29,13 +29,21 @@
 
 #import "RTCTypes.h"
 
+@class RTCMediaStreamTrack;
+@protocol RTCMediaStreamTrackDelegate<NSObject>
+
+- (void)mediaStreamTrackDidChange:(RTCMediaStreamTrack*)mediaStreamTrack;
+
+@end
+
 // RTCMediaStreamTrack implements the interface common to RTCAudioTrack and
 // RTCVideoTrack.  Do not create an instance of this class, rather create one
 // of the derived classes.
 @interface RTCMediaStreamTrack : NSObject
 
-@property(nonatomic, assign, readonly) NSString *kind;
-@property(nonatomic, assign, readonly) NSString *label;
+@property(nonatomic, readonly) NSString* kind;
+@property(nonatomic, readonly) NSString* label;
+@property(nonatomic, weak) id<RTCMediaStreamTrackDelegate> delegate;
 
 - (BOOL)isEnabled;
 - (BOOL)setEnabled:(BOOL)enabled;
diff --git a/talk/app/webrtc/objc/public/RTCVideoRenderer.h b/talk/app/webrtc/objc/public/RTCVideoRenderer.h
index d6b6a65..f78746c 100644
--- a/talk/app/webrtc/objc/public/RTCVideoRenderer.h
+++ b/talk/app/webrtc/objc/public/RTCVideoRenderer.h
@@ -26,27 +26,40 @@
  */
 
 #import <Foundation/Foundation.h>
-#import <QuartzCore/QuartzCore.h>
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
 
-@protocol RTCVideoRendererDelegate;
-struct CGRect;
-@class UIView;
+@class RTCI420Frame;
+@class RTCVideoRenderer;
+
+// RTCVideoRendererDelegate is a protocol for an object that must be
+// implemented to get messages when rendering.
+@protocol RTCVideoRendererDelegate<NSObject>
+
+// The size of the frame.
+- (void)renderer:(RTCVideoRenderer*)renderer didSetSize:(CGSize)size;
+
+// The frame to be displayed.
+- (void)renderer:(RTCVideoRenderer*)renderer
+    didReceiveFrame:(RTCI420Frame*)frame;
+
+@end
 
 // Interface for rendering VideoFrames from a VideoTrack
 @interface RTCVideoRenderer : NSObject
 
-@property(nonatomic, strong) id<RTCVideoRendererDelegate> delegate;
-
-- (id)initWithView:(UIView*)view;
+@property(nonatomic, weak) id<RTCVideoRendererDelegate> delegate;
 
 // Initialize the renderer.  Requires a delegate which does the actual drawing
 // of frames.
-- (id)initWithDelegate:(id<RTCVideoRendererDelegate>)delegate;
+- (instancetype)initWithDelegate:(id<RTCVideoRendererDelegate>)delegate;
 
-// Starts rendering.
-- (void)start;
-// Stops rendering. It can be restarted again using the 'start' method above.
-- (void)stop;
+#if TARGET_OS_IPHONE
+// DEPRECATED. See https://code.google.com/p/webrtc/issues/detail?id=3341 for
+// details.
+- (instancetype)initWithView:(UIView*)view;
+#endif
 
 #ifndef DOXYGEN_SHOULD_SKIP_THIS
 // Disallow init and don't add to documentation
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m
index cc33f03..87d1f53 100644
--- a/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m
+++ b/talk/examples/ios/AppRTCDemo/APPRTCAppDelegate.m
@@ -30,6 +30,7 @@
 #import "APPRTCAppDelegate.h"
 
 #import "APPRTCViewController.h"
+#import "RTCEAGLVideoView.h"
 #import "RTCICECandidate.h"
 #import "RTCICEServer.h"
 #import "RTCMediaConstraints.h"
@@ -43,13 +44,12 @@
 #import "RTCVideoRenderer.h"
 #import "RTCVideoCapturer.h"
 #import "RTCVideoTrack.h"
-#import "APPRTCVideoView.h"
 
 @interface PCObserver : NSObject<RTCPeerConnectionDelegate>
 
 - (id)initWithDelegate:(id<APPRTCSendMessage>)delegate;
 
-@property(nonatomic, strong) APPRTCVideoView* videoView;
+@property(nonatomic, strong) RTCEAGLVideoView* videoView;
 
 @end
 
@@ -89,8 +89,7 @@
       NSAssert([stream.videoTracks count] <= 1,
                @"Expected at most 1 video stream");
       if ([stream.videoTracks count] != 0) {
-        [self.videoView
-            renderVideoTrackInterface:[stream.videoTracks objectAtIndex:0]];
+        self.videoView.videoTrack = stream.videoTracks[0];
       }
   });
 }
@@ -291,13 +290,12 @@
   if (localVideoTrack) {
     [lms addVideoTrack:localVideoTrack];
   }
+  self.viewController.localVideoView.videoTrack = localVideoTrack;
+#else
+  self.viewController.localVideoView.hidden = YES;
 #endif
 
-  [self.viewController.localVideoView
-      renderVideoTrackInterface:localVideoTrack];
-
   self.pcObserver.videoView = self.viewController.remoteVideoView;
-
   [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
   [self.peerConnection addStream:lms constraints:constraints];
   [self displayLogMessage:@"onICEServers - added local stream."];
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.m b/talk/examples/ios/AppRTCDemo/APPRTCVideoView.m
deleted file mode 100644
index 23466b6..0000000
--- a/talk/examples/ios/AppRTCDemo/APPRTCVideoView.m
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * libjingle
- * Copyright 2013, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/*
- * This APPRTCVideoView must be initialzed and added to a View to get
- * either the local or remote video stream rendered.
- * It is a view itself and it encapsulates
- * an object of VideoRenderIosView and UIActivityIndicatorView.
- * Both of the views will get resized as per the frame of their parent.
- */
-
-#import "APPRTCVideoView.h"
-
-#import "RTCVideoRenderer.h"
-#import "RTCVideoTrack.h"
-
-@interface APPRTCVideoView () {
-  RTCVideoTrack* _track;
-  RTCVideoRenderer* _renderer;
-}
-
-@property(nonatomic, weak) UIView* renderView;
-@property(nonatomic, weak) UIActivityIndicatorView* activityView;
-
-@end
-
-@implementation APPRTCVideoView
-
-@synthesize videoOrientation = _videoOrientation;
-
-- (void)layoutSubviews {
-  [super layoutSubviews];
-  if (!_renderer) {
-    // Left-right (mirror) flip the remote view.
-    CGAffineTransform xform =
-        CGAffineTransformMakeScale(self.isRemote ? -1 : 1, 1);
-    // TODO(fischman): why is this rotate (vertical+horizontal flip) needed?!?
-    xform = CGAffineTransformRotate(xform, M_PI);
-    // TODO(fischman): ensure back-camera flip is correct in all orientations,
-    // when back-camera support is added.
-    [self setTransform:xform];
-    _renderer = [[RTCVideoRenderer alloc] initWithView:self];
-  }
-}
-
-- (void)renderVideoTrackInterface:(RTCVideoTrack*)videoTrack {
-  [_track removeRenderer:_renderer];
-  [_renderer stop];
-
-  _track = videoTrack;
-
-  if (_track) {
-    [_track addRenderer:_renderer];
-    [_renderer start];
-  }
-}
-
-@end
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCViewController.h b/talk/examples/ios/AppRTCDemo/APPRTCViewController.h
index f5fcee4..1737a13 100644
--- a/talk/examples/ios/AppRTCDemo/APPRTCViewController.h
+++ b/talk/examples/ios/AppRTCDemo/APPRTCViewController.h
@@ -27,7 +27,7 @@
 
 #import <UIKit/UIKit.h>
 
-@class APPRTCVideoView;
+@class RTCEAGLVideoView;
 
 // The view controller that is displayed when AppRTCDemo is loaded.
 @interface APPRTCViewController : UIViewController<UITextFieldDelegate>
@@ -37,8 +37,8 @@
 @property(weak, nonatomic) IBOutlet UITextView* logView;
 @property(weak, nonatomic) IBOutlet UIView* blackView;
 
-@property(nonatomic, strong) APPRTCVideoView* remoteVideoView;
-@property(nonatomic, strong) APPRTCVideoView* localVideoView;
+@property(nonatomic, strong) RTCEAGLVideoView* localVideoView;
+@property(nonatomic, strong) RTCEAGLVideoView* remoteVideoView;
 
 - (void)displayText:(NSString*)text;
 - (void)resetUI;
diff --git a/talk/examples/ios/AppRTCDemo/APPRTCViewController.m b/talk/examples/ios/AppRTCDemo/APPRTCViewController.m
index 0ac9282..bdd8b50 100644
--- a/talk/examples/ios/AppRTCDemo/APPRTCViewController.m
+++ b/talk/examples/ios/AppRTCDemo/APPRTCViewController.m
@@ -27,12 +27,11 @@
 
 #import "APPRTCViewController.h"
 
-#import "APPRTCVideoView.h"
+#import <AVFoundation/AVFoundation.h>
+#import "RTCEAGLVideoView.h"
 
 @interface APPRTCViewController ()
-
 @property(nonatomic, assign) UIInterfaceOrientation statusBarOrientation;
-
 @end
 
 @implementation APPRTCViewController
@@ -75,12 +74,10 @@
   self.logView.text = nil;
   self.blackView.hidden = YES;
 
-  [_remoteVideoView renderVideoTrackInterface:nil];
-  [_remoteVideoView removeFromSuperview];
+  [self.remoteVideoView removeFromSuperview];
   self.remoteVideoView = nil;
 
-  [_localVideoView renderVideoTrackInterface:nil];
-  [_localVideoView removeFromSuperview];
+  [self.localVideoView removeFromSuperview];
   self.localVideoView = nil;
 }
 
@@ -97,46 +94,29 @@
 - (void)setupCaptureSession {
   self.blackView.hidden = NO;
 
-  CGRect frame =
-      CGRectMake((self.blackView.bounds.size.width - kRemoteVideoWidth) / 2,
-                 (self.blackView.bounds.size.height - kRemoteVideoHeight) / 2,
-                 kRemoteVideoWidth,
-                 kRemoteVideoHeight);
-  APPRTCVideoView* videoView = [[APPRTCVideoView alloc] initWithFrame:frame];
-  videoView.isRemote = TRUE;
+  CGSize videoSize =
+        CGSizeMake(kRemoteVideoWidth, kRemoteVideoHeight);
+  CGRect remoteVideoFrame =
+      AVMakeRectWithAspectRatioInsideRect(videoSize,
+                                          self.blackView.bounds);
+  CGRect localVideoFrame = remoteVideoFrame;
+  // TODO(tkchin): use video dimensions from incoming video stream
+  // and handle rotation.
+  localVideoFrame.size.width = remoteVideoFrame.size.height / 4;
+  localVideoFrame.size.height = remoteVideoFrame.size.width / 4;
+  localVideoFrame.origin.x = CGRectGetMaxX(remoteVideoFrame)
+      - localVideoFrame.size.width - kLocalViewPadding;
+  localVideoFrame.origin.y = CGRectGetMaxY(remoteVideoFrame)
+      - localVideoFrame.size.height - kLocalViewPadding;
 
-  [self.blackView addSubview:videoView];
-  videoView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
-                               UIViewAutoresizingFlexibleRightMargin |
-                               UIViewAutoresizingFlexibleBottomMargin |
-                               UIViewAutoresizingFlexibleTopMargin;
-  videoView.translatesAutoresizingMaskIntoConstraints = YES;
-  _remoteVideoView = videoView;
+  self.remoteVideoView =
+      [[RTCEAGLVideoView alloc] initWithFrame:remoteVideoFrame];
+  [self.blackView addSubview:self.remoteVideoView];
+  self.remoteVideoView.transform = CGAffineTransformMakeScale(-1, 1);
 
-  CGSize screenSize = [[UIScreen mainScreen] bounds].size;
-  CGFloat localVideoViewWidth =
-      UIInterfaceOrientationIsPortrait(self.statusBarOrientation)
-          ? screenSize.width / 4
-          : screenSize.height / 4;
-  CGFloat localVideoViewHeight =
-      UIInterfaceOrientationIsPortrait(self.statusBarOrientation)
-          ? screenSize.height / 4
-          : screenSize.width / 4;
-  frame = CGRectMake(self.blackView.bounds.size.width - localVideoViewWidth -
-                         kLocalViewPadding,
-                     kLocalViewPadding,
-                     localVideoViewWidth,
-                     localVideoViewHeight);
-  videoView = [[APPRTCVideoView alloc] initWithFrame:frame];
-  videoView.isRemote = FALSE;
-
-  [self.blackView addSubview:videoView];
-  videoView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
-                               UIViewAutoresizingFlexibleBottomMargin |
-                               UIViewAutoresizingFlexibleHeight |
-                               UIViewAutoresizingFlexibleWidth;
-  videoView.translatesAutoresizingMaskIntoConstraints = YES;
-  _localVideoView = videoView;
+  self.localVideoView =
+      [[RTCEAGLVideoView alloc] initWithFrame:localVideoFrame];
+  [self.blackView addSubview:self.localVideoView];
 }
 
 #pragma mark - UITextFieldDelegate
diff --git a/talk/examples/ios/AppRTCDemo/Info.plist b/talk/examples/ios/AppRTCDemo/Info.plist
index 72504aa..a32be86 100644
--- a/talk/examples/ios/AppRTCDemo/Info.plist
+++ b/talk/examples/ios/AppRTCDemo/Info.plist
@@ -70,8 +70,6 @@
         <key>UISupportedInterfaceOrientations</key>
         <array>
                 <string>UIInterfaceOrientationPortrait</string>
-                <string>UIInterfaceOrientationLandscapeLeft</string>
-                <string>UIInterfaceOrientationLandscapeRight</string>
         </array>
 </dict>
 </plist>
diff --git a/talk/libjingle.gyp b/talk/libjingle.gyp
index 1a25cce..8bb2847 100755
--- a/talk/libjingle.gyp
+++ b/talk/libjingle.gyp
@@ -173,6 +173,7 @@
             'app/webrtc/objc/RTCDataChannel.mm',
             'app/webrtc/objc/RTCEnumConverter.h',
             'app/webrtc/objc/RTCEnumConverter.mm',
+            'app/webrtc/objc/RTCI420Frame+Internal.h',
             'app/webrtc/objc/RTCI420Frame.mm',
             'app/webrtc/objc/RTCICECandidate+Internal.h',
             'app/webrtc/objc/RTCICECandidate.mm',
@@ -227,7 +228,6 @@
             'app/webrtc/objc/public/RTCTypes.h',
             'app/webrtc/objc/public/RTCVideoCapturer.h',
             'app/webrtc/objc/public/RTCVideoRenderer.h',
-            'app/webrtc/objc/public/RTCVideoRendererDelegate.h',
             'app/webrtc/objc/public/RTCVideoSource.h',
             'app/webrtc/objc/public/RTCVideoTrack.h',
           ],
@@ -243,7 +243,6 @@
           ],
           'link_settings': {
             'libraries': [
-              '$(SDKROOT)/System/Library/Frameworks/Foundation.framework',
               '-lstdc++',
             ],
           },
@@ -254,6 +253,23 @@
             'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO',
           },
           'conditions': [
+            ['OS=="ios"', {
+              'sources': [
+                'app/webrtc/objc/RTCEAGLVideoRenderer.mm',
+                'app/webrtc/objc/RTCEAGLVideoView+Internal.h',
+                'app/webrtc/objc/RTCEAGLVideoView.m',
+                'app/webrtc/objc/public/RTCEAGLVideoRenderer.h',
+                'app/webrtc/objc/public/RTCEAGLVideoView.h',
+              ],
+              'link_settings': {
+                'xcode_settings': {
+                  'OTHER_LDFLAGS': [
+                    '-framework CoreGraphics',
+                    '-framework GLKit',
+                  ],
+                },
+              },
+            }],
             ['OS=="mac"', {
               'xcode_settings': {
                 # Need to build against 10.7 framework for full ARC support
@@ -680,7 +696,6 @@
             'xcode_settings': {
               'OTHER_LDFLAGS': [
                 '-framework Foundation',
-                '-framework IOKit',
                 '-framework Security',
                 '-framework SystemConfiguration',
                 '-framework UIKit',
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index 0648ec0..dc8b653 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -248,8 +248,6 @@
             'examples/ios/AppRTCDemo/APPRTCAppDelegate.m',
             'examples/ios/AppRTCDemo/APPRTCViewController.h',
             'examples/ios/AppRTCDemo/APPRTCViewController.m',
-            'examples/ios/AppRTCDemo/APPRTCVideoView.h',
-            'examples/ios/AppRTCDemo/APPRTCVideoView.m',
             'examples/ios/AppRTCDemo/AppRTCDemo-Prefix.pch',
             'examples/ios/AppRTCDemo/GAEChannelClient.h',
             'examples/ios/AppRTCDemo/GAEChannelClient.m',
@@ -258,11 +256,6 @@
           'xcode_settings': {
             'CLANG_ENABLE_OBJC_ARC': 'YES',
             'INFOPLIST_FILE': 'examples/ios/AppRTCDemo/Info.plist',
-            'OTHER_LDFLAGS': [
-              '-framework CoreGraphics',
-              '-framework Foundation',
-              '-framework UIKit',
-            ],
           },
         },  # target AppRTCDemo
       ],  # targets