@@ -125,6 +125,7 @@ @interface StubFVPDisplayLinkFactory : NSObject <FVPDisplayLinkFactory>
125125
126126/* * This display link to return. */
127127@property (nonatomic , strong ) FVPDisplayLink *displayLink;
128+ @property (nonatomic ) void (^fireDisplayLink)(void );
128129
129130- (instancetype )initWithDisplayLink : (FVPDisplayLink *)displayLink ;
130131
@@ -138,6 +139,7 @@ - (instancetype)initWithDisplayLink:(FVPDisplayLink *)displayLink {
138139}
139140- (FVPDisplayLink *)displayLinkWithRegistrar : (id <FlutterPluginRegistrar>)registrar
140141 callback : (void (^)(void ))callback {
142+ self.fireDisplayLink = callback;
141143 return self.displayLink ;
142144}
143145
@@ -243,13 +245,14 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily {
243245 OCMStub ([mockVideoOutput hasNewPixelBufferForItemTime: kCMTimeZero ])
244246 .ignoringNonObjectArgs ()
245247 .andReturn (YES );
246- // Any non-zero value is fine here since it won't actually be used, just NULL-checked.
247- CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef) 1 ;
248+ CVPixelBufferRef bufferRef;
249+ CVPixelBufferCreate ( NULL , 1 , 1 , kCVPixelFormatType_32BGRA , NULL , &bufferRef) ;
248250 OCMStub ([mockVideoOutput copyPixelBufferForItemTime: kCMTimeZero itemTimeForDisplay: NULL ])
249251 .ignoringNonObjectArgs ()
250- .andReturn (fakeBufferRef );
252+ .andReturn (bufferRef );
251253 // Simulate a callback from the engine to request a new frame.
252- [player copyPixelBuffer ];
254+ stubDisplayLinkFactory.fireDisplayLink ();
255+ CFRelease ([player copyPixelBuffer ]);
253256 // Since a frame was found, and the video is paused, the display link should be paused again.
254257 OCMVerify ([mockDisplayLink setRunning: NO ]);
255258}
@@ -294,14 +297,15 @@ - (void)testInitStartsDisplayLinkTemporarily {
294297 OCMStub ([mockVideoOutput hasNewPixelBufferForItemTime: kCMTimeZero ])
295298 .ignoringNonObjectArgs ()
296299 .andReturn (YES );
297- // Any non-zero value is fine here since it won't actually be used, just NULL-checked.
298- CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef) 1 ;
300+ CVPixelBufferRef bufferRef;
301+ CVPixelBufferCreate ( NULL , 1 , 1 , kCVPixelFormatType_32BGRA , NULL , &bufferRef) ;
299302 OCMStub ([mockVideoOutput copyPixelBufferForItemTime: kCMTimeZero itemTimeForDisplay: NULL ])
300303 .ignoringNonObjectArgs ()
301- .andReturn (fakeBufferRef );
304+ .andReturn (bufferRef );
302305 // Simulate a callback from the engine to request a new frame.
303306 FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId [textureId];
304- [player copyPixelBuffer ];
307+ stubDisplayLinkFactory.fireDisplayLink ();
308+ CFRelease ([player copyPixelBuffer ]);
305309 // Since a frame was found, and the video is paused, the display link should be paused again.
306310 OCMVerify ([mockDisplayLink setRunning: NO ]);
307311}
@@ -357,13 +361,14 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
357361 OCMStub ([mockVideoOutput hasNewPixelBufferForItemTime: kCMTimeZero ])
358362 .ignoringNonObjectArgs ()
359363 .andReturn (YES );
360- // Any non-zero value is fine here since it won't actually be used, just NULL-checked.
361- CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef) 1 ;
364+ CVPixelBufferRef bufferRef;
365+ CVPixelBufferCreate ( NULL , 1 , 1 , kCVPixelFormatType_32BGRA , NULL , &bufferRef) ;
362366 OCMStub ([mockVideoOutput copyPixelBufferForItemTime: kCMTimeZero itemTimeForDisplay: NULL ])
363367 .ignoringNonObjectArgs ()
364- .andReturn (fakeBufferRef );
368+ .andReturn (bufferRef );
365369 // Simulate a callback from the engine to request a new frame.
366- [player copyPixelBuffer ];
370+ stubDisplayLinkFactory.fireDisplayLink ();
371+ CFRelease ([player copyPixelBuffer ]);
367372 // Since the video was playing, the display link should not be paused after getting a buffer.
368373 OCMVerify (never (), [mockDisplayLink setRunning: NO ]);
369374}
@@ -790,6 +795,82 @@ - (void)testPublishesInRegistration {
790795 XCTAssertTrue ([publishedValue isKindOfClass: [FVPVideoPlayerPlugin class ]]);
791796}
792797
798+ - (void )testPlayerShouldNotDropEverySecondFrame {
799+ NSObject <FlutterPluginRegistrar> *registrar =
800+ [GetPluginRegistry () registrarForPlugin: @" testPlayerShouldNotDropEverySecondFrame" ];
801+ NSObject <FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock (registrar);
802+ NSObject <FlutterTextureRegistry> *mockTextureRegistry =
803+ OCMProtocolMock (@protocol (FlutterTextureRegistry));
804+ OCMStub ([partialRegistrar textures ]).andReturn (mockTextureRegistry);
805+
806+ FVPDisplayLink *displayLink = [[FVPDisplayLink alloc ] initWithRegistrar: registrar
807+ callback: ^(){
808+ }];
809+ StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
810+ [[StubFVPDisplayLinkFactory alloc ] initWithDisplayLink: displayLink];
811+ AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock ([[AVPlayerItemVideoOutput alloc ] init ]);
812+ FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc ]
813+ initWithAVFactory: [[StubFVPAVFactory alloc ] initWithPlayer: nil output: mockVideoOutput]
814+ displayLinkFactory: stubDisplayLinkFactory
815+ registrar: partialRegistrar];
816+
817+ FlutterError *error;
818+ [videoPlayerPlugin initialize: &error];
819+ XCTAssertNil (error);
820+ FVPCreationOptions *create = [FVPCreationOptions
821+ makeWithAsset: nil
822+ uri: @" https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
823+ packageName: nil
824+ formatHint: nil
825+ httpHeaders: @{}];
826+ NSNumber *textureId = [videoPlayerPlugin createWithOptions: create error: &error];
827+ FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId [textureId];
828+
829+ __block CMTime currentTime = kCMTimeZero ;
830+ OCMStub ([mockVideoOutput itemTimeForHostTime: 0 ])
831+ .ignoringNonObjectArgs ()
832+ .andDo (^(NSInvocation *invocation) {
833+ [invocation setReturnValue: ¤tTime];
834+ });
835+ __block NSMutableSet *pixelBuffers = NSMutableSet .new ;
836+ OCMStub ([mockVideoOutput hasNewPixelBufferForItemTime: kCMTimeZero ])
837+ .ignoringNonObjectArgs ()
838+ .andDo (^(NSInvocation *invocation) {
839+ CMTime itemTime;
840+ [invocation getArgument: &itemTime atIndex: 2 ];
841+ BOOL has = [pixelBuffers containsObject: [NSValue valueWithCMTime: itemTime]];
842+ [invocation setReturnValue: &has];
843+ });
844+ OCMStub ([mockVideoOutput copyPixelBufferForItemTime: kCMTimeZero
845+ itemTimeForDisplay: [OCMArg anyPointer ]])
846+ .ignoringNonObjectArgs ()
847+ .andDo (^(NSInvocation *invocation) {
848+ CMTime itemTime;
849+ [invocation getArgument: &itemTime atIndex: 2 ];
850+ CVPixelBufferRef bufferRef = NULL ;
851+ if ([pixelBuffers containsObject: [NSValue valueWithCMTime: itemTime]]) {
852+ CVPixelBufferCreate (NULL , 1 , 1 , kCVPixelFormatType_32BGRA , NULL , &bufferRef);
853+ }
854+ [pixelBuffers removeObject: [NSValue valueWithCMTime: itemTime]];
855+ [invocation setReturnValue: &bufferRef];
856+ });
857+ void (^advanceFrame)(void ) = ^{
858+ currentTime.value ++;
859+ [pixelBuffers addObject: [NSValue valueWithCMTime: currentTime]];
860+ };
861+
862+ advanceFrame ();
863+ OCMExpect ([mockTextureRegistry textureFrameAvailable: textureId.intValue]);
864+ stubDisplayLinkFactory.fireDisplayLink ();
865+ OCMVerifyAllWithDelay (mockTextureRegistry, 10 );
866+
867+ advanceFrame ();
868+ OCMExpect ([mockTextureRegistry textureFrameAvailable: textureId.intValue]);
869+ CFRelease ([player copyPixelBuffer ]);
870+ stubDisplayLinkFactory.fireDisplayLink ();
871+ OCMVerifyAllWithDelay (mockTextureRegistry, 10 );
872+ }
873+
793874#if TARGET_OS_IOS
794875- (void )validateTransformFixForOrientation : (UIImageOrientation)orientation {
795876 AVAssetTrack *track = [[FakeAVAssetTrack alloc ] initWithOrientation: orientation];
0 commit comments