From 0ebe0199acd1070f17ca2abc5bc22fdd8b0861ca Mon Sep 17 00:00:00 2001 From: denicija Date: Fri, 3 Mar 2017 08:24:06 -0800 Subject: [PATCH] Add unit tests for RTCMTLVideoView. To properly test the functionality, following changes were needed - Make RTCMTLVideoView compiliable for all cpu architectures not just arm64. This is needed so that the test can run on any device and on simulator as well. - Refactor RTCMTLVideoView to have mockable class methods. The unittest class, RTCMTLVideoViewTests was designed to provide easy transition to XCTest when the time comes for that. To transition to XCTest it would suffice to inherit from XCTestCase and remove the gtest methods. BUG=webrtc:7079 Review-Url: https://codereview.webrtc.org/2723903003 Cr-Commit-Position: refs/heads/master@{#17014} --- webrtc/sdk/BUILD.gn | 7 + .../Classes/Metal/RTCMTLNV12Renderer.h | 16 +- .../Framework/Classes/Metal/RTCMTLVideoView.m | 61 +++++-- .../UnitTests/RTCMTLVideoViewTests.mm | 168 ++++++++++++++++++ 4 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 webrtc/sdk/objc/Framework/UnitTests/RTCMTLVideoViewTests.mm diff --git a/webrtc/sdk/BUILD.gn b/webrtc/sdk/BUILD.gn index 4acf3783f5..a789efb57e 100644 --- a/webrtc/sdk/BUILD.gn +++ b/webrtc/sdk/BUILD.gn @@ -94,6 +94,7 @@ if (is_ios || is_mac) { rtc_static_library("rtc_sdk_peerconnection_objc") { sources = [ + "objc/Framework/Classes/Metal/RTCMTLNV12Renderer.h", "objc/Framework/Classes/RTCAVFoundationVideoCapturerInternal.h", "objc/Framework/Classes/RTCAVFoundationVideoCapturerInternal.mm", "objc/Framework/Classes/RTCAVFoundationVideoSource+Private.h", @@ -257,6 +258,12 @@ if (is_ios || is_mac) { "objc/Framework/UnitTests/RTCSessionDescriptionTest.mm", "objc/Framework/UnitTests/avformatmappertests.mm", ] + if (is_ios) { + sources += [ "objc/Framework/UnitTests/RTCMTLVideoViewTests.mm" ] + if (current_cpu != "arm64") { + sources += [ "objc/Framework/Classes/Metal/RTCMTLVideoView.m" ] + } + } deps = [ ":rtc_sdk_peerconnection_objc", "//third_party/ocmock", diff --git a/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLNV12Renderer.h b/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLNV12Renderer.h index 0508e78db5..4f7b25eefa 100644 --- a/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLNV12Renderer.h +++ b/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLNV12Renderer.h @@ -24,12 +24,6 @@ * @param frame The frame to be rendered. */ - (void)drawFrame:(RTCVideoFrame *)frame; -@end - -/** - * Implementation of RTCMTLRenderer protocol for rendering native nv12 video frames. - */ -@interface RTCMTLNV12Renderer : NSObject /** * Sets the provided view as rendering destination if possible. @@ -37,5 +31,13 @@ * If not possible method returns NO and callers of the method are responisble for performing * cleanups. */ -- (BOOL)addRenderingDestination:(__kindof MTKView *)view; +- (BOOL)addRenderingDestination:(__kindof UIView *)view; + +@end + +/** + * Implementation of RTCMTLRenderer protocol for rendering native nv12 video frames. + */ +@interface RTCMTLNV12Renderer : NSObject + @end diff --git a/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m b/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m index 63ef23ad49..427ae6c4a1 100644 --- a/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m +++ b/webrtc/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m @@ -18,15 +18,18 @@ #import "RTCMTLNV12Renderer.h" +// To avoid unreconized symbol linker errors, we're taking advantage of the objc runtime. +// Linking errors occur when compiling for architectures that don't support Metal. +#define MTKViewClass NSClassFromString(@"MTKView") +#define RTCMTLNV12RendererClass NSClassFromString(@"RTCMTLNV12Renderer") + @interface RTCMTLVideoView () -@property(nonatomic, strong) id renderer; +@property(nonatomic, strong) RTCMTLNV12Renderer *renderer; @property(nonatomic, strong) MTKView *metalView; @property(atomic, strong) RTCVideoFrame *videoFrame; @end -@implementation RTCMTLVideoView { - id _renderer; -} +@implementation RTCMTLVideoView @synthesize renderer = _renderer; @synthesize metalView = _metalView; @@ -58,11 +61,41 @@ #endif } ++ (MTKView *)createMetalView:(CGRect)frame { + MTKView *view = [[MTKViewClass alloc] initWithFrame:frame]; + return view; +} + ++ (RTCMTLNV12Renderer *)createMetalRenderer { + RTCMTLNV12Renderer *renderer = [[RTCMTLNV12RendererClass alloc] init]; + return renderer; +} + - (void)configure { - if ([RTCMTLVideoView isMetalAvailable]) { - _metalView = [[MTKView alloc] initWithFrame:self.bounds]; - [self addSubview:_metalView]; + if (![RTCMTLVideoView isMetalAvailable]) { + RTCLog("Metal unavailable"); + return; + } + + _metalView = [RTCMTLVideoView createMetalView:self.bounds]; + _renderer = [RTCMTLVideoView createMetalRenderer]; + + if ([self configureMetalRenderer]) { + [self configureMetalView]; + } else { + _renderer = nil; + RTCLogError("Metal configuration falied."); + } +} + +- (BOOL)configureMetalRenderer { + return [_renderer addRenderingDestination:_metalView]; +} + +- (void)configureMetalView { + if (_metalView) { _metalView.delegate = self; + [self addSubview:_metalView]; _metalView.contentMode = UIViewContentModeScaleAspectFit; _metalView.translatesAutoresizingMaskIntoConstraints = NO; UILayoutGuide *margins = self.layoutMarginsGuide; @@ -70,20 +103,14 @@ [_metalView.bottomAnchor constraintEqualToAnchor:margins.bottomAnchor].active = YES; [_metalView.leftAnchor constraintEqualToAnchor:margins.leftAnchor].active = YES; [_metalView.rightAnchor constraintEqualToAnchor:margins.rightAnchor].active = YES; - - _renderer = [[RTCMTLNV12Renderer alloc] init]; - if (![(RTCMTLNV12Renderer *)_renderer addRenderingDestination:_metalView]) { - _renderer = nil; - }; - } else { - RTCLogError("Metal configuration falied."); } } + #pragma mark - MTKViewDelegate methods - (void)drawInMTKView:(nonnull MTKView *)view { NSAssert(view == self.metalView, @"Receiving draw callbacks from foreign instance."); - [_renderer drawFrame:self.videoFrame]; + [self.renderer drawFrame:self.videoFrame]; } - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { @@ -92,8 +119,8 @@ #pragma mark - RTCVideoRenderer - (void)setSize:(CGSize)size { - _metalView.drawableSize = size; - [_metalView draw]; + self.metalView.drawableSize = size; + [self.metalView draw]; } - (void)renderFrame:(nullable RTCVideoFrame *)frame { diff --git a/webrtc/sdk/objc/Framework/UnitTests/RTCMTLVideoViewTests.mm b/webrtc/sdk/objc/Framework/UnitTests/RTCMTLVideoViewTests.mm new file mode 100644 index 0000000000..1d422eb09a --- /dev/null +++ b/webrtc/sdk/objc/Framework/UnitTests/RTCMTLVideoViewTests.mm @@ -0,0 +1,168 @@ +/* + * Copyright 2017 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 +#import + +#include "webrtc/base/gunit.h" + +#include +#include + +// Extension of RTCMTLVideoView for testing purposes. +@interface RTCMTLVideoView (Testing) +@property(nonatomic, strong) id renderer; +@property(nonatomic, strong) UIView* metalView; +@property(atomic, strong) RTCVideoFrame* videoFrame; + ++ (BOOL)isMetalAvailable; ++ (UIView*)createMetalView:(CGRect)frame; ++ (id)createMetalRenderer; +- (void)drawInMTKView:(id)view; +@end + +@interface RTCMTLVideoViewTests : NSObject +@property(nonatomic, strong) id classMock; +@property(nonatomic, strong) id metalViewMock; +@property(nonatomic, strong) id rendererMock; +@end + +@implementation RTCMTLVideoViewTests + +@synthesize classMock = _classMock; +@synthesize metalViewMock = _metalViewMock; +@synthesize rendererMock = _rendererMock; + +- (void)setup { + self.classMock = OCMClassMock([RTCMTLVideoView class]); + + self.metalViewMock = OCMClassMock([RTCMTLVideoViewTests class]); + // NOTE: OCMock doesen't provide modern syntax for -ignoringNonObjectArgs. + [[[[self.classMock stub] ignoringNonObjectArgs] andReturn:self.metalViewMock] + createMetalView:CGRectZero]; + + self.rendererMock = OCMProtocolMock(@protocol(RTCMTLRenderer)); + OCMStub([self.classMock createMetalRenderer]).andReturn(self.rendererMock); +} + +- (void)tearDown { + [self.classMock stopMocking]; + [self.rendererMock stopMocking]; + [self.metalViewMock stopMocking]; + self.classMock = nil; + self.rendererMock = nil; + self.metalViewMock = nil; +} + +- (void)testMetalConfigureNotExecuted { + // when + OCMStub([self.classMock isMetalAvailable]).andReturn(NO); + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + + // then + EXPECT_TRUE(realView.renderer == nil); + EXPECT_TRUE(realView.metalView == nil); +} + +- (void)testMetalConfigureExecuted { + // given + OCMStub([self.classMock isMetalAvailable]).andReturn(YES); + OCMStub([self.rendererMock addRenderingDestination:self.metalViewMock]) + .andReturn(NO); + + // when + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + + // then + EXPECT_TRUE(realView.renderer == nil); + EXPECT_TRUE(realView.metalView != nil); +} + +- (void)testMetalDrawCallback { + // given + OCMStub([self.classMock isMetalAvailable]).andReturn(NO); + OCMExpect([self.rendererMock drawFrame:[OCMArg any]]); + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + realView.metalView = self.metalViewMock; + realView.renderer = self.rendererMock; + + // when + [realView drawInMTKView:self.metalViewMock]; + + // then + [self.rendererMock verify]; +} + +- (void)testRTCVideoRenderNilFrameCallback { + // given + OCMStub([self.classMock isMetalAvailable]).andReturn(NO); + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + + // when + [realView renderFrame:nil]; + + // then + EXPECT_TRUE(realView.videoFrame == nil); +} + +- (void)testRTCVideoRenderFrameCallback { + // given + OCMStub([self.classMock isMetalAvailable]).andReturn(NO); + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + id frame = OCMClassMock([RTCVideoFrame class]); + realView.metalView = self.metalViewMock; + realView.renderer = self.rendererMock; + OCMExpect([self.rendererMock drawFrame:frame]); + + // when + [realView renderFrame:frame]; + [realView drawInMTKView:self.metalViewMock]; + + // then + EXPECT_EQ(realView.videoFrame, frame); + [self.rendererMock verify]; +} +@end + +TEST(RTCMTLVideoViewTests, MetalConfigureNotExecuted) { + RTCMTLVideoViewTests *test = [[RTCMTLVideoViewTests alloc] init]; + [test setup]; + [test testMetalConfigureNotExecuted]; + [test tearDown]; +} + + +TEST(RTCMTLVideoViewTests, MetalConfigureExecuted) { + RTCMTLVideoViewTests *test = [[RTCMTLVideoViewTests alloc] init]; + [test setup]; + [test testMetalConfigureExecuted]; + [test tearDown]; +} + +TEST(RTCMTLVideoViewTests, MetalDrawCallback) { + RTCMTLVideoViewTests *test = [[RTCMTLVideoViewTests alloc] init]; + [test setup]; + [test testMetalDrawCallback]; + [test tearDown]; +} + +TEST(RTCMTLVideoViewTests, RTCVideoRenderNilFrameCallback) { + RTCMTLVideoViewTests *test = [[RTCMTLVideoViewTests alloc] init]; + [test setup]; + [test testRTCVideoRenderNilFrameCallback]; + [test tearDown]; +} + +TEST(RTCMTLVideoViewTests, RTCVideoRenderFrameCallback) { + RTCMTLVideoViewTests *test = [[RTCMTLVideoViewTests alloc] init]; + [test setup]; + [test testRTCVideoRenderFrameCallback]; + [test tearDown]; +}