diff --git a/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m b/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m index 0fd8a1a912..24dc29f7f3 100644 --- a/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m +++ b/sdk/objc/Framework/Classes/Metal/RTCMTLVideoView.m @@ -33,7 +33,9 @@ @property(atomic, strong) RTCVideoFrame *videoFrame; @end -@implementation RTCMTLVideoView +@implementation RTCMTLVideoView { + int64_t _lastFrameTimeNs; +} @synthesize rendererI420 = _rendererI420; @synthesize rendererNV12 = _rendererNV12; @@ -106,7 +108,8 @@ - (void)drawInMTKView:(nonnull MTKView *)view { NSAssert(view == self.metalView, @"Receiving draw callbacks from foreign instance."); RTCVideoFrame *videoFrame = self.videoFrame; - if (!videoFrame) { + // Skip rendering if we've already rendered this frame. + if (!videoFrame || videoFrame.timeStampNs == _lastFrameTimeNs) { return; } @@ -116,6 +119,7 @@ if (![self.rendererNV12 addRenderingDestination:self.metalView]) { self.rendererNV12 = nil; RTCLogError(@"Failed to create NV12 renderer"); + return; } } [self.rendererNV12 drawFrame:videoFrame]; @@ -125,10 +129,12 @@ if (![self.rendererI420 addRenderingDestination:self.metalView]) { self.rendererI420 = nil; RTCLogError(@"Failed to create I420 renderer"); + return; } } [self.rendererI420 drawFrame:videoFrame]; } + _lastFrameTimeNs = videoFrame.timeStampNs; } - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { diff --git a/sdk/objc/Framework/UnitTests/RTCMTLVideoView_xctest.mm b/sdk/objc/Framework/UnitTests/RTCMTLVideoView_xctest.mm index 782bc64b1a..8533a31066 100644 --- a/sdk/objc/Framework/UnitTests/RTCMTLVideoView_xctest.mm +++ b/sdk/objc/Framework/UnitTests/RTCMTLVideoView_xctest.mm @@ -73,6 +73,7 @@ } else { OCMStub([frameMock buffer]).andReturn([[RTCI420Buffer alloc] initWithWidth:200 height:200]); } + OCMStub([frameMock timeStampNs]).andReturn(arc4random_uniform(INT_MAX)); return frameMock; } @@ -196,4 +197,54 @@ [self.classMock verify]; } +- (void)testDontRedrawOldFrame { + OCMStub([self.classMock isMetalAvailable]).andReturn(YES); + self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES]; + self.frameMock = [self frameMockWithCVPixelBuffer:YES]; + + OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]); + OCMExpect([self.classMock createNV12Renderer]).andReturn(self.rendererNV12Mock); + [[self.classMock reject] createI420Renderer]; + + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + [realView renderFrame:self.frameMock]; + [realView drawInMTKView:nil]; + + [self.rendererNV12Mock verify]; + [self.classMock verify]; + + [[self.rendererNV12Mock reject] drawFrame:[OCMArg any]]; + + [realView renderFrame:self.frameMock]; + [realView drawInMTKView:nil]; + + [self.rendererNV12Mock verify]; +} + +- (void)testDoDrawNewFrame { + OCMStub([self.classMock isMetalAvailable]).andReturn(YES); + self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES]; + self.frameMock = [self frameMockWithCVPixelBuffer:YES]; + + OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]); + OCMExpect([self.classMock createNV12Renderer]).andReturn(self.rendererNV12Mock); + [[self.classMock reject] createI420Renderer]; + + RTCMTLVideoView *realView = [[RTCMTLVideoView alloc] init]; + [realView renderFrame:self.frameMock]; + [realView drawInMTKView:nil]; + + [self.rendererNV12Mock verify]; + [self.classMock verify]; + + // Get new frame. + self.frameMock = [self frameMockWithCVPixelBuffer:YES]; + OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]); + + [realView renderFrame:self.frameMock]; + [realView drawInMTKView:nil]; + + [self.rendererNV12Mock verify]; +} + @end