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]; +}