diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 9c6f47ee7dd..81e43153e7a 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.0 + +* Adds support for platform views as an optional way of displaying a video. + ## 2.6.7 * Fixes playback speed resetting. diff --git a/packages/video_player/video_player_avfoundation/README.md b/packages/video_player/video_player_avfoundation/README.md index 1325daf0acf..f46470ee2f5 100644 --- a/packages/video_player/video_player_avfoundation/README.md +++ b/packages/video_player/video_player_avfoundation/README.md @@ -13,3 +13,7 @@ should add it to your `pubspec.yaml` as usual. [1]: https://pub.dev/packages/video_player [2]: https://flutter.dev/to/endorsed-federated-plugin + +## Platform limitations + +On macOS, the plugin does not currently support platform views. Instead, a texture view is always used to display the video player, even if `VideoViewType.platformView` is specified as a parameter. \ No newline at end of file diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index 4150250d168..4e6e13db93f 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -8,7 +8,13 @@ #import #import +#import #import +#import + +#if TARGET_OS_IOS +#import +#endif // TODO(stuartmorgan): Convert to using mock registrars instead. NSObject *GetPluginRegistry(void) { @@ -120,7 +126,7 @@ - (AVPlayerItemVideoOutput *)videoOutputWithPixelBufferAttributes: #pragma mark - -/** Test implementation of FVPDisplayLinkFactory that returns a provided display link nstance. */ +/** Test implementation of FVPDisplayLinkFactory that returns a provided display link instance. */ @interface StubFVPDisplayLinkFactory : NSObject /** This display link to return. */ @@ -159,6 +165,35 @@ - (FVPDisplayLink *)displayLinkWithRegistrar:(id)registr @implementation VideoPlayerTests +- (void)testCreateWithOptionsReturnsErrorForInvalidAssetPath { + NSObject *registrar = [GetPluginRegistry() + registrarForPlugin:@"testCreateWithOptionsReturnsErrorForInvalidAssetPath"]; + FVPVideoPlayerPlugin *videoPlayerPlugin = + [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); + + id mockRegistrar = OCMPartialMock(registrar); + OCMStub([mockRegistrar lookupKeyForAsset:[OCMArg any]]).andReturn(nil); + + FVPCreationOptions *create = + [FVPCreationOptions makeWithAsset:@"invalid/path/to/asset" + uri:nil + packageName:nil + formatHint:nil + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + + FlutterError *createError; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&createError]; + + XCTAssertNil(playerIdentifier); + XCTAssertNotNil(createError); + XCTAssertEqualObjects(createError.code, @"video_player"); +} + - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some @@ -179,17 +214,54 @@ - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSom uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; XCTAssertNil(error); - XCTAssertNotNil(textureId); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + XCTAssertNotNil(playerIdentifier); + FVPTextureBasedVideoPlayer *player = + (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present."); XCTAssertNotNil(player.playerLayer.superlayer, @"AVPlayerLayer should be added on screen."); } +- (void)testPlayerForPlatformViewDoesNotRegisterTexture { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + NSObject *registrar = + [GetPluginRegistry() registrarForPlugin:@"testPlayerForPlatformViewDoesNotRegisterTexture"]; + NSObject *partialRegistrar = OCMPartialMock(registrar); + OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); + FVPDisplayLink *mockDisplayLink = + OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar + callback:^(){ + }]); + StubFVPDisplayLinkFactory *stubDisplayLinkFactory = + [[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink]; + AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]); + FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] + initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput] + displayLinkFactory:stubDisplayLinkFactory + registrar:partialRegistrar]; + + FlutterError *initalizationError; + [videoPlayerPlugin initialize:&initalizationError]; + XCTAssertNil(initalizationError); + FVPCreationOptions *create = [FVPCreationOptions + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" + packageName:nil + formatHint:nil + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypePlatformView]; + FlutterError *createError; + [videoPlayerPlugin createWithOptions:create error:&createError]; + + OCMVerify(never(), [mockTextureRegistry registerTexture:[OCMArg any]]); +} + - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { NSObject *mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); @@ -217,17 +289,18 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&createError]; // Ensure that the video playback is paused before seeking. FlutterError *pauseError; - [videoPlayerPlugin pausePlayer:textureId.integerValue error:&pauseError]; + [videoPlayerPlugin pausePlayer:playerIdentifier.integerValue error:&pauseError]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo completes"]; [videoPlayerPlugin seekTo:1234 - forPlayer:textureId.integerValue + forPlayer:playerIdentifier.integerValue completion:^(FlutterError *_Nullable error) { [initializedExpectation fulfill]; }]; @@ -235,9 +308,14 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { // Seeking to a new position should start the display link temporarily. OCMVerify([mockDisplayLink setRunning:YES]); - - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; - XCTAssertEqual([player position], 1234); + FVPTextureBasedVideoPlayer *player = + (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[playerIdentifier]; + // Wait for the player's position to update, it shouldn't take long. + XCTestExpectation *positionExpectation = + [self expectationForPredicate:[NSPredicate predicateWithFormat:@"position == 1234"] + evaluatedWithObject:player + handler:nil]; + [self waitForExpectations:@[ positionExpectation ] timeout:3.0]; // Simulate a buffer being available. OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero]) @@ -283,9 +361,10 @@ - (void)testInitStartsDisplayLinkTemporarily { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&createError]; // Init should start the display link temporarily. OCMVerify([mockDisplayLink setRunning:YES]); @@ -300,7 +379,8 @@ - (void)testInitStartsDisplayLinkTemporarily { .ignoringNonObjectArgs() .andReturn(fakeBufferRef); // Simulate a callback from the engine to request a new frame. - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPTextureBasedVideoPlayer *player = + (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[playerIdentifier]; [player copyPixelBuffer]; // Since a frame was found, and the video is paused, the display link should be paused again. OCMVerify([mockDisplayLink setRunning:NO]); @@ -333,24 +413,26 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&createError]; // Ensure that the video is playing before seeking. FlutterError *playError; - [videoPlayerPlugin playPlayer:textureId.integerValue error:&playError]; + [videoPlayerPlugin playPlayer:playerIdentifier.integerValue error:&playError]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo completes"]; [videoPlayerPlugin seekTo:1234 - forPlayer:textureId.integerValue + forPlayer:playerIdentifier.integerValue completion:^(FlutterError *_Nullable error) { [initializedExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; OCMVerify([mockDisplayLink setRunning:YES]); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPTextureBasedVideoPlayer *player = + (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertEqual([player position], 1234); // Simulate a buffer being available. @@ -395,14 +477,15 @@ - (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&createError]; // Run a play/pause cycle to force the pause codepath to run completely. FlutterError *playPauseError; - [videoPlayerPlugin playPlayer:textureId.integerValue error:&playPauseError]; - [videoPlayerPlugin pausePlayer:textureId.integerValue error:&playPauseError]; + [videoPlayerPlugin playPlayer:playerIdentifier.integerValue error:&playPauseError]; + [videoPlayerPlugin pausePlayer:playerIdentifier.integerValue error:&playPauseError]; // Since a buffer hasn't been available yet, the pause should not have stopped the display link. OCMVerify(never(), [mockDisplayLink setRunning:NO]); @@ -423,18 +506,19 @@ - (void)testDeregistersFromPlayer { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; XCTAssertNil(error); - XCTAssertNotNil(textureId); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + XCTAssertNotNil(playerIdentifier); + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); AVPlayer *avPlayer = player.player; [self keyValueObservingExpectationForObject:avPlayer keyPath:@"currentItem" expectedValue:nil]; - [videoPlayerPlugin disposePlayer:textureId.integerValue error:&error]; - XCTAssertEqual(videoPlayerPlugin.playersByTextureId.count, 0); + [videoPlayerPlugin disposePlayer:playerIdentifier.integerValue error:&error]; + XCTAssertEqual(videoPlayerPlugin.playersByIdentifier.count, 0); XCTAssertNil(error); [self waitForExpectationsWithTimeout:30.0 handler:nil]; @@ -455,11 +539,12 @@ - (void)testBufferingStateFromPlayer { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; XCTAssertNil(error); - XCTAssertNotNil(textureId); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + XCTAssertNotNil(playerIdentifier); + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); AVPlayer *avPlayer = player.player; [avPlayer play]; @@ -580,14 +665,15 @@ - (void)testSeekToleranceWhenNotSeekingToEnd { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [pluginWithMockAVPlayer createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [pluginWithMockAVPlayer createWithOptions:create error:&createError]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"]; [pluginWithMockAVPlayer seekTo:1234 - forPlayer:textureId.integerValue + forPlayer:playerIdentifier.integerValue completion:^(FlutterError *_Nullable error) { [initializedExpectation fulfill]; }]; @@ -618,15 +704,16 @@ - (void)testSeekToleranceWhenSeekingToEnd { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; FlutterError *createError; - NSNumber *textureId = [pluginWithMockAVPlayer createWithOptions:create error:&createError]; + NSNumber *playerIdentifier = [pluginWithMockAVPlayer createWithOptions:create error:&createError]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"]; // The duration of this video is "0" due to the non standard initiliatazion process. [pluginWithMockAVPlayer seekTo:0 - forPlayer:textureId.integerValue + forPlayer:playerIdentifier.integerValue completion:^(FlutterError *_Nullable error) { [initializedExpectation fulfill]; }]; @@ -641,14 +728,16 @@ - (void)testSeekToleranceWhenSeekingToEnd { [videoPlayerPlugin initialize:&error]; XCTAssertNil(error); - FVPCreationOptions *create = [FVPCreationOptions makeWithAsset:nil - uri:uri - packageName:nil - formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + FVPCreationOptions *create = + [FVPCreationOptions makeWithAsset:nil + uri:uri + packageName:nil + formatHint:nil + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; @@ -670,15 +759,15 @@ - (void)testSeekToleranceWhenSeekingToEnd { XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusPaused); // Change playback speed. - [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:playerIdentifier.integerValue error:&error]; XCTAssertNil(error); - [videoPlayerPlugin playPlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin playPlayer:playerIdentifier.integerValue error:&error]; XCTAssertNil(error); XCTAssertEqual(avPlayer.rate, 2); XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate); // Volume - [videoPlayerPlugin setVolume:0.1 forPlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin setVolume:0.1 forPlayer:playerIdentifier.integerValue error:&error]; XCTAssertNil(error); XCTAssertEqual(avPlayer.volume, 0.1f); @@ -712,17 +801,18 @@ - (void)testDoesNotCrashOnRateObservationAfterDisposal { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; XCTAssertNil(error); - XCTAssertNotNil(textureId); + XCTAssertNotNil(playerIdentifier); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); weakPlayer = player; avPlayer = player.player; - [videoPlayerPlugin disposePlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin disposePlayer:playerIdentifier.integerValue error:&error]; XCTAssertNil(error); } @@ -766,12 +856,14 @@ - (void)testHotReloadDoesNotCrash { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; XCTAssertNil(error); - XCTAssertNotNil(textureId); + XCTAssertNotNil(playerIdentifier); - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPTextureBasedVideoPlayer *player = + (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); weakPlayer = player; @@ -795,12 +887,34 @@ - (void)testHotReloadDoesNotCrash { handler:nil]; // No assertions needed. Lack of crash is a success. } +#if TARGET_OS_IOS +- (void)testNativeVideoViewFactoryRegistration { + NSObject *registry = GetPluginRegistry(); + NSObject *registrar = + [registry registrarForPlugin:@"testNativeVideoViewFactoryRegistration"]; + id mockRegistrar = OCMPartialMock(registrar); + + OCMExpect([mockRegistrar + registerViewFactory:[OCMArg isKindOfClass:[FVPNativeVideoViewFactory class]] + withId:@"plugins.flutter.dev/video_player_ios"]); + [FVPVideoPlayerPlugin registerWithRegistrar:mockRegistrar]; + + OCMVerifyAll(mockRegistrar); +} +#endif + - (void)testPublishesInRegistration { NSString *pluginKey = @"TestRegistration"; NSObject *registry = GetPluginRegistry(); NSObject *registrar = [registry registrarForPlugin:pluginKey]; + id mockRegistrar = OCMPartialMock(registrar); + // Empty stub to pass a check in Flutter's engine (double factory registration). + // registerWithRegistrar gets called at the beginning of the test, and factory is registered + // there. Additional call would try to register the same factory another time, which would fail a + // check in the engine. + OCMStub([mockRegistrar registerViewFactory:[OCMArg any] withId:[OCMArg any]]); - [FVPVideoPlayerPlugin registerWithRegistrar:registrar]; + [FVPVideoPlayerPlugin registerWithRegistrar:mockRegistrar]; id publishedValue = [registry valuePublishedByPlugin:pluginKey]; @@ -817,13 +931,15 @@ - (void)testFailedToLoadVideoEventShouldBeAlwaysSent { [videoPlayerPlugin initialize:&error]; - FVPCreationOptions *create = [FVPCreationOptions makeWithAsset:nil - uri:@"" - packageName:nil - formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + FVPCreationOptions *create = + [FVPCreationOptions makeWithAsset:nil + uri:@"" + packageName:nil + formatHint:nil + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTAssertNotNil(player); [self keyValueObservingExpectationForObject:(id)player.player.currentItem @@ -858,9 +974,10 @@ - (void)testUpdatePlayingStateShouldNotResetRate { uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" packageName:nil formatHint:nil - httpHeaders:@{}]; - NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error]; - FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; [player onListenWithArguments:nil @@ -871,8 +988,8 @@ - (void)testUpdatePlayingStateShouldNotResetRate { }]; [self waitForExpectationsWithTimeout:10 handler:nil]; - [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error]; - [videoPlayerPlugin playPlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:playerIdentifier.integerValue error:&error]; + [videoPlayerPlugin playPlayer:playerIdentifier.integerValue error:&error]; XCTAssertEqual(player.player.rate, 2); } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPFrameUpdater.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPFrameUpdater.m index 435699879f1..e73d486da17 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPFrameUpdater.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPFrameUpdater.m @@ -25,7 +25,7 @@ - (void)displayLinkFired { CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { _lastKnownAvailableTime = outputItemTime; - [_registry textureFrameAvailable:_textureId]; + [_registry textureFrameAvailable:_textureIdentifier]; } } @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m new file mode 100644 index 00000000000..509d0b5e55a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m @@ -0,0 +1,198 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h" +#import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h" + +@interface FVPTextureBasedVideoPlayer () +// The CALayer associated with the Flutter view this plugin is associated with, if any. +@property(nonatomic, readonly) CALayer *flutterViewLayer; +// The updater that drives callbacks to the engine to indicate that a new frame is ready. +@property(nonatomic) FVPFrameUpdater *frameUpdater; +// The display link that drives frameUpdater. +@property(nonatomic) FVPDisplayLink *displayLink; +// Whether a new frame needs to be provided to the engine regardless of the current play/pause state +// (e.g., after a seek while paused). If YES, the display link should continue to run until the next +// frame is successfully provided. +@property(nonatomic, assign) BOOL waitingForFrame; +@property(nonatomic, copy) void (^onDisposed)(int64_t); +@end + +@implementation FVPTextureBasedVideoPlayer +- (instancetype)initWithAsset:(NSString *)asset + frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink + avFactory:(id)avFactory + registrar:(NSObject *)registrar + onDisposed:(void (^)(int64_t))onDisposed { + return [self initWithURL:[NSURL fileURLWithPath:[FVPVideoPlayer absolutePathForAssetName:asset]] + frameUpdater:frameUpdater + displayLink:displayLink + httpHeaders:@{} + avFactory:avFactory + registrar:registrar + onDisposed:onDisposed]; +} + +- (instancetype)initWithURL:(NSURL *)url + frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink + httpHeaders:(nonnull NSDictionary *)headers + avFactory:(id)avFactory + registrar:(NSObject *)registrar + onDisposed:(void (^)(int64_t))onDisposed { + NSDictionary *options = nil; + if ([headers count] != 0) { + options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers}; + } + AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; + AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; + return [self initWithPlayerItem:item + frameUpdater:frameUpdater + displayLink:displayLink + avFactory:avFactory + registrar:registrar + onDisposed:onDisposed]; +} + +- (instancetype)initWithPlayerItem:(AVPlayerItem *)item + frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink + avFactory:(id)avFactory + registrar:(NSObject *)registrar + onDisposed:(void (^)(int64_t))onDisposed { + self = [super initWithPlayerItem:item avFactory:avFactory registrar:registrar]; + + if (self) { + _frameUpdater = frameUpdater; + _displayLink = displayLink; + _frameUpdater.videoOutput = self.videoOutput; + _onDisposed = [onDisposed copy]; + + // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 + // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some + // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An + // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams + // for issue #1, and restore the correct width and height for issue #2. + _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; + [self.flutterViewLayer addSublayer:self.playerLayer]; + } + return self; +} + +- (void)setTextureIdentifier:(int64_t)textureIdentifier { + self.frameUpdater.textureIdentifier = textureIdentifier; +} + +- (void)expectFrame { + self.waitingForFrame = YES; + + _displayLink.running = YES; +} + +#pragma mark - Private methods + +- (CALayer *)flutterViewLayer { +#if TARGET_OS_OSX + return self.registrar.view.layer; +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(hellohuanlin): Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; +#pragma clang diagnostic pop + return root.view.layer; +#endif +} + +#pragma mark - Overrides + +- (void)updatePlayingState { + [super updatePlayingState]; + // If the texture is still waiting for an expected frame, the display link needs to keep + // running until it arrives regardless of the play/pause state. + _displayLink.running = self.isPlaying || self.waitingForFrame; +} + +- (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHandler { + CMTime previousCMTime = self.player.currentTime; + [super seekTo:location + completionHandler:^(BOOL completed) { + if (CMTimeCompare(self.player.currentTime, previousCMTime) != 0) { + // Ensure that a frame is drawn once available, even if currently paused. In theory a + // race is possible here where the new frame has already drawn by the time this code + // runs, and the display link stays on indefinitely, but that should be relatively + // harmless. This must use the display link rather than just informing the engine that a + // new frame is available because the seek completing doesn't guarantee that the pixel + // buffer is already available. + [self expectFrame]; + } + + if (completionHandler) { + completionHandler(completed); + } + }]; +} + +- (void)disposeSansEventChannel { + // This check prevents the crash caused by removing the KVO observers twice. + // When performing a Hot Restart, the leftover players are disposed once directly + // by [FVPVideoPlayerPlugin initialize:] method and then disposed again by + // [FVPVideoPlayer onTextureUnregistered:] call leading to possible over-release. + if (self.disposed) { + return; + } + + [super disposeSansEventChannel]; + + [self.playerLayer removeFromSuperlayer]; + + _displayLink = nil; +} + +- (void)dispose { + [super dispose]; + + _onDisposed(self.frameUpdater.textureIdentifier); +} + +#pragma mark - FlutterTexture + +- (CVPixelBufferRef)copyPixelBuffer { + CVPixelBufferRef buffer = NULL; + CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()]; + if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { + buffer = [self.videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; + } else { + // If the current time isn't available yet, use the time that was checked when informing the + // engine that a frame was available (if any). + CMTime lastAvailableTime = self.frameUpdater.lastKnownAvailableTime; + if (CMTIME_IS_VALID(lastAvailableTime)) { + buffer = [self.videoOutput copyPixelBufferForItemTime:lastAvailableTime + itemTimeForDisplay:NULL]; + } + } + + if (self.waitingForFrame && buffer) { + self.waitingForFrame = NO; + // If the display link was only running temporarily to pick up a new frame while the video was + // paused, stop it again. + if (!self.isPlaying) { + self.displayLink.running = NO; + } + } + + return buffer; +} + +- (void)onTextureUnregistered:(NSObject *)texture { + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self.disposed) { + [self dispose]; + } + }); +} + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 087cf401db5..d1d096e7b28 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "./include/video_player_avfoundation/FVPVideoPlayer.h" +#import "./include/video_player_avfoundation/FVPVideoPlayer_Internal.h" #import "./include/video_player_avfoundation/FVPVideoPlayer_Test.h" #import @@ -16,61 +17,17 @@ static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; static void *rateContext = &rateContext; -@interface FVPVideoPlayer () -/// The AVPlayerItemVideoOutput associated with this video player. -@property(nonatomic, readonly) AVPlayerItemVideoOutput *videoOutput; -/// The plugin registrar, to obtain view information from. -@property(nonatomic, readonly) NSObject *registrar; -/// The CALayer associated with the Flutter view this plugin is associated with, if any. -@property(nonatomic, readonly) CALayer *flutterViewLayer; -/// The Flutter event sink used to send events to the Flutter engine. -@property(nonatomic) FlutterEventSink eventSink; -/// The preferred transform for the video. It can be used to handle the rotation of the video. -@property(nonatomic) CGAffineTransform preferredTransform; -/// Indicates whether the video player is currently playing. -@property(nonatomic, readonly) BOOL isPlaying; -/// The target playback speed requested by the plugin client. -@property(nonatomic, readonly) NSNumber *targetPlaybackSpeed; -/// Indicates whether the video player has been initialized. -@property(nonatomic, readonly) BOOL isInitialized; -/// The updater that drives callbacks to the engine to indicate that a new frame is ready. -@property(nonatomic) FVPFrameUpdater *frameUpdater; -/// The display link that drives frameUpdater. -@property(nonatomic) FVPDisplayLink *displayLink; -/// Whether a new frame needs to be provided to the engine regardless of the current play/pause -/// state (e.g., after a seek while paused). If YES, the display link should continue to run until -/// the next frame is successfully provided. -@property(nonatomic, assign) BOOL waitingForFrame; - -/// Updates the playing state of the video player. -- (void)updatePlayingState; -@end - @implementation FVPVideoPlayer - (instancetype)initWithAsset:(NSString *)asset - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink avFactory:(id)avFactory registrar:(NSObject *)registrar { - NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; -#if TARGET_OS_OSX - // See https://github.com/flutter/flutter/issues/135302 - // TODO(stuartmorgan): Remove this if the asset APIs are adjusted to work better for macOS. - if (!path) { - path = [NSURL URLWithString:asset relativeToURL:NSBundle.mainBundle.bundleURL].path; - } -#endif - return [self initWithURL:[NSURL fileURLWithPath:path] - frameUpdater:frameUpdater - displayLink:displayLink + return [self initWithURL:[NSURL fileURLWithPath:[FVPVideoPlayer absolutePathForAssetName:asset]] httpHeaders:@{} avFactory:avFactory registrar:registrar]; } - (instancetype)initWithURL:(NSURL *)url - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink httpHeaders:(nonnull NSDictionary *)headers avFactory:(id)avFactory registrar:(NSObject *)registrar { @@ -80,23 +37,16 @@ - (instancetype)initWithURL:(NSURL *)url } AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; - return [self initWithPlayerItem:item - frameUpdater:frameUpdater - displayLink:(FVPDisplayLink *)displayLink - avFactory:avFactory - registrar:registrar]; + return [self initWithPlayerItem:item avFactory:avFactory registrar:registrar]; } - (instancetype)initWithPlayerItem:(AVPlayerItem *)item - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink avFactory:(id)avFactory registrar:(NSObject *)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registrar = registrar; - _frameUpdater = frameUpdater; AVAsset *asset = [item asset]; void (^assetCompletionHandler)(void) = ^{ @@ -130,22 +80,12 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item _player = [avFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 - // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some - // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An - // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams - // for issue #1, and restore the correct width and height for issue #2. - _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; - [self.flutterViewLayer addSublayer:_playerLayer]; - // Configure output. - _displayLink = displayLink; NSDictionary *pixBuffAttributes = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), (id)kCVPixelBufferIOSurfacePropertiesKey : @{} }; _videoOutput = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes]; - frameUpdater.videoOutput = _videoOutput; [self addObserversForItem:item player:_player]; @@ -160,6 +100,19 @@ - (void)dealloc { } } ++ (NSString *)absolutePathForAssetName:(NSString *)assetName { + NSString *path = [[NSBundle mainBundle] pathForResource:assetName ofType:nil]; +#if TARGET_OS_OSX + // See https://github.com/flutter/flutter/issues/135302 + // TODO(stuartmorgan): Remove this if the asset APIs are adjusted to work better for macOS. + if (!path) { + path = [NSURL URLWithString:assetName relativeToURL:NSBundle.mainBundle.bundleURL].path; + } +#endif + + return path; +} + - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { [item addObserver:self forKeyPath:@"loadedTimeRanges" @@ -337,9 +290,6 @@ - (void)updatePlayingState { } else { [_player pause]; } - // If the texture is still waiting for an expected frame, the display link needs to keep - // running until it arrives regardless of the play/pause state. - _displayLink.running = _isPlaying || self.waitingForFrame; } /// Synchronizes the player's playback rate with targetPlaybackSpeed, constrained by the playback @@ -468,7 +418,6 @@ - (int64_t)duration { } - (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHandler { - CMTime previousCMTime = _player.currentTime; CMTime targetCMTime = CMTimeMake(location, 1000); CMTimeValue duration = _player.currentItem.asset.duration.value; // Without adding tolerance when seeking to duration, @@ -479,27 +428,12 @@ - (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHan toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL completed) { - if (CMTimeCompare(self.player.currentTime, previousCMTime) != 0) { - // Ensure that a frame is drawn once available, even if currently paused. In theory a race - // is possible here where the new frame has already drawn by the time this code runs, and - // the display link stays on indefinitely, but that should be relatively harmless. This - // must use the display link rather than just informing the engine that a new frame is - // available because the seek completing doesn't guarantee that the pixel buffer is - // already available. - [self expectFrame]; - } - if (completionHandler) { completionHandler(completed); } }]; } -- (void)expectFrame { - self.waitingForFrame = YES; - self.displayLink.running = YES; -} - - (void)setIsLooping:(BOOL)isLooping { _isLooping = isLooping; } @@ -513,38 +447,6 @@ - (void)setPlaybackSpeed:(double)speed { [self updatePlayingState]; } -- (CVPixelBufferRef)copyPixelBuffer { - CVPixelBufferRef buffer = NULL; - CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; - if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { - buffer = [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; - } else { - // If the current time isn't available yet, use the time that was checked when informing the - // engine that a frame was available (if any). - CMTime lastAvailableTime = self.frameUpdater.lastKnownAvailableTime; - if (CMTIME_IS_VALID(lastAvailableTime)) { - buffer = [_videoOutput copyPixelBufferForItemTime:lastAvailableTime itemTimeForDisplay:NULL]; - } - } - - if (self.waitingForFrame && buffer) { - self.waitingForFrame = NO; - // If the display link was only running temporarily to pick up a new frame while the video was - // paused, stop it again. - if (!self.isPlaying) { - self.displayLink.running = NO; - } - } - - return buffer; -} - -- (void)onTextureUnregistered:(NSObject *)texture { - dispatch_async(dispatch_get_main_queue(), ^{ - [self dispose]; - }); -} - - (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { _eventSink = nil; return nil; @@ -573,17 +475,7 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments /// is useful for the case where the Engine is in the process of deconstruction /// so the channel is going to die or is already dead. - (void)disposeSansEventChannel { - // This check prevents the crash caused by removing the KVO observers twice. - // When performing a Hot Restart, the leftover players are disposed once directly - // by [FVPVideoPlayerPlugin initialize:] method and then disposed again by - // [FVPVideoPlayer onTextureUnregistered:] call leading to possible over-release. - if (_disposed) { - return; - } - _disposed = YES; - [_playerLayer removeFromSuperlayer]; - _displayLink = nil; [self removeKeyValueObservers]; [self.player replaceCurrentItemWithPlayerItem:nil]; @@ -595,20 +487,6 @@ - (void)dispose { [_eventChannel setStreamHandler:nil]; } -- (CALayer *)flutterViewLayer { -#if TARGET_OS_OSX - return self.registrar.view.layer; -#else -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(hellohuanlin): Provide a non-deprecated codepath. See - // https://github.com/flutter/flutter/issues/104117 - UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; -#pragma clang diagnostic pop - return root.view.layer; -#endif -} - /// Removes all key-value observers set up for the player. /// /// This is called from dealloc, so must not use any methods on self. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index 0834b10d1ae..1936435ffd0 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -10,9 +10,17 @@ #import "./include/video_player_avfoundation/FVPAVFactory.h" #import "./include/video_player_avfoundation/FVPDisplayLink.h" #import "./include/video_player_avfoundation/FVPFrameUpdater.h" +#import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h" #import "./include/video_player_avfoundation/FVPVideoPlayer.h" +// Relative path is needed for messages.g.h. See +// https://github.com/flutter/packages/pull/6675/#discussion_r1591210702 #import "./include/video_player_avfoundation/messages.g.h" +#if TARGET_OS_IOS +// Platform views are only supported on iOS as of now. +#import "./include/video_player_avfoundation/FVPNativeVideoViewFactory.h" +#endif + #if !__has_feature(objc_arc) #error Code Requires ARC. #endif @@ -37,12 +45,23 @@ @interface FVPVideoPlayerPlugin () @property(readonly, strong, nonatomic) NSObject *registrar; @property(nonatomic, strong) id displayLinkFactory; @property(nonatomic, strong) id avFactory; +// TODO(stuartmorgan): Decouple identifiers for platform views and texture views. +@property(nonatomic, assign) int64_t nextNonTexturePlayerIdentifier; @end @implementation FVPVideoPlayerPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FVPVideoPlayerPlugin *instance = [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar]; [registrar publish:instance]; +#if TARGET_OS_IOS + // Platform views are only supported on iOS as of now. + FVPNativeVideoViewFactory *factory = [[FVPNativeVideoViewFactory alloc] + initWithMessenger:registrar.messenger + playerByIdentifierProvider:^FVPVideoPlayer *(NSNumber *playerIdentifier) { + return instance->_playersByIdentifier[playerIdentifier]; + }]; + [registrar registerViewFactory:factory withId:@"plugins.flutter.dev/video_player_ios"]; +#endif SetUpFVPAVFoundationVideoPlayerApi(registrar.messenger, instance); } @@ -62,32 +81,47 @@ - (instancetype)initWithAVFactory:(id)avFactory _registrar = registrar; _displayLinkFactory = displayLinkFactory ?: [[FVPDefaultDisplayLinkFactory alloc] init]; _avFactory = avFactory ?: [[FVPDefaultAVFactory alloc] init]; - _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1]; + _playersByIdentifier = [NSMutableDictionary dictionaryWithCapacity:1]; + // Initialized to a high number to avoid collisions with texture identifiers (which are generated + // separately). + _nextNonTexturePlayerIdentifier = INT_MAX; return self; } - (void)detachFromEngineForRegistrar:(NSObject *)registrar { - [self.playersByTextureId.allValues makeObjectsPerformSelector:@selector(disposeSansEventChannel)]; - [self.playersByTextureId removeAllObjects]; + [self.playersByIdentifier.allValues + makeObjectsPerformSelector:@selector(disposeSansEventChannel)]; + [self.playersByIdentifier removeAllObjects]; SetUpFVPAVFoundationVideoPlayerApi(registrar.messenger, nil); } -- (int64_t)onPlayerSetup:(FVPVideoPlayer *)player frameUpdater:(FVPFrameUpdater *)frameUpdater { - int64_t textureId = [self.registry registerTexture:player]; - frameUpdater.textureId = textureId; +- (int64_t)onPlayerSetup:(FVPVideoPlayer *)player { + FVPTextureBasedVideoPlayer *textureBasedPlayer = + [player isKindOfClass:[FVPTextureBasedVideoPlayer class]] + ? (FVPTextureBasedVideoPlayer *)player + : nil; + + int64_t playerIdentifier; + if (textureBasedPlayer) { + playerIdentifier = [self.registry registerTexture:(FVPTextureBasedVideoPlayer *)player]; + [textureBasedPlayer setTextureIdentifier:playerIdentifier]; + } else { + playerIdentifier = self.nextNonTexturePlayerIdentifier--; + } + FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%lld", - textureId] + playerIdentifier] binaryMessenger:_messenger]; [eventChannel setStreamHandler:player]; player.eventChannel = eventChannel; - self.playersByTextureId[@(textureId)] = player; + self.playersByIdentifier[@(playerIdentifier)] = player; // Ensure that the first frame is drawn once available, even if the video isn't played, since // the engine is now expecting the texture to be populated. - [player expectFrame]; + [textureBasedPlayer expectFrame]; - return textureId; + return playerIdentifier; } // This function, although slightly modified, is also in camera_avfoundation. @@ -132,16 +166,32 @@ - (void)initialize:(FlutterError *__autoreleasing *)error { upgradeAudioSessionCategory(AVAudioSessionCategoryPlayback, 0, 0); #endif - [self.playersByTextureId - enumerateKeysAndObjectsUsingBlock:^(NSNumber *textureId, FVPVideoPlayer *player, BOOL *stop) { - [self.registry unregisterTexture:textureId.unsignedIntegerValue]; - [player dispose]; - }]; - [self.playersByTextureId removeAllObjects]; + [self.playersByIdentifier.allValues makeObjectsPerformSelector:@selector(dispose)]; + [self.playersByIdentifier removeAllObjects]; } - (nullable NSNumber *)createWithOptions:(nonnull FVPCreationOptions *)options error:(FlutterError **)error { + BOOL textureBased = options.viewType == FVPPlatformVideoViewTypeTextureView; + + @try { + FVPVideoPlayer *player = textureBased ? [self texturePlayerWithOptions:options] + : [self platformViewPlayerWithOptions:options]; + + if (player == nil) { + *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; + return nil; + } + + return @([self onPlayerSetup:player]); + } @catch (NSException *exception) { + *error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil]; + return nil; + } +} + +- (nullable FVPTextureBasedVideoPlayer *)texturePlayerWithOptions: + (nonnull FVPCreationOptions *)options { FVPFrameUpdater *frameUpdater = [[FVPFrameUpdater alloc] initWithRegistry:_registry]; FVPDisplayLink *displayLink = [self.displayLinkFactory displayLinkWithRegistrar:_registrar @@ -149,78 +199,101 @@ - (nullable NSNumber *)createWithOptions:(nonnull FVPCreationOptions *)options [frameUpdater displayLinkFired]; }]; - FVPVideoPlayer *player; + __weak typeof(self) weakSelf = self; + void (^onDisposed)(int64_t) = ^(int64_t textureIdentifier) { + [weakSelf.registry unregisterTexture:textureIdentifier]; + }; + if (options.asset) { - NSString *assetPath; - if (options.packageName) { - assetPath = [_registrar lookupKeyForAsset:options.asset fromPackage:options.packageName]; - } else { - assetPath = [_registrar lookupKeyForAsset:options.asset]; - } - @try { - player = [[FVPVideoPlayer alloc] initWithAsset:assetPath - frameUpdater:frameUpdater - displayLink:displayLink - avFactory:_avFactory - registrar:self.registrar]; - return @([self onPlayerSetup:player frameUpdater:frameUpdater]); - } @catch (NSException *exception) { - *error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil]; - return nil; - } + NSString *assetPath = [self assetPathFromCreationOptions:options]; + return [[FVPTextureBasedVideoPlayer alloc] initWithAsset:assetPath + frameUpdater:frameUpdater + displayLink:displayLink + avFactory:_avFactory + registrar:self.registrar + onDisposed:onDisposed]; } else if (options.uri) { - player = [[FVPVideoPlayer alloc] initWithURL:[NSURL URLWithString:options.uri] - frameUpdater:frameUpdater - displayLink:displayLink - httpHeaders:options.httpHeaders + return [[FVPTextureBasedVideoPlayer alloc] initWithURL:[NSURL URLWithString:options.uri] + frameUpdater:frameUpdater + displayLink:displayLink + httpHeaders:options.httpHeaders + avFactory:_avFactory + registrar:self.registrar + onDisposed:onDisposed]; + } + + return nil; +} + +- (nullable FVPVideoPlayer *)platformViewPlayerWithOptions:(nonnull FVPCreationOptions *)options { + // FVPVideoPlayer contains all required logic for platform views. + if (options.asset) { + NSString *assetPath = [self assetPathFromCreationOptions:options]; + return [[FVPVideoPlayer alloc] initWithAsset:assetPath avFactory:_avFactory registrar:self.registrar]; - return @([self onPlayerSetup:player frameUpdater:frameUpdater]); - } else { - *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; - return nil; + } else if (options.uri) { + return [[FVPVideoPlayer alloc] initWithURL:[NSURL URLWithString:options.uri] + httpHeaders:options.httpHeaders + avFactory:_avFactory + registrar:self.registrar]; } + + return nil; } -- (void)disposePlayer:(NSInteger)textureId error:(FlutterError **)error { - NSNumber *playerKey = @(textureId); - FVPVideoPlayer *player = self.playersByTextureId[playerKey]; - [self.registry unregisterTexture:textureId]; - [self.playersByTextureId removeObjectForKey:playerKey]; - if (!player.disposed) { - [player dispose]; +- (NSString *)assetPathFromCreationOptions:(nonnull FVPCreationOptions *)options { + NSString *assetPath; + if (options.packageName) { + assetPath = [self.registrar lookupKeyForAsset:options.asset fromPackage:options.packageName]; + } else { + assetPath = [self.registrar lookupKeyForAsset:options.asset]; } + return assetPath; +} + +- (void)disposePlayer:(NSInteger)playerIdentifier error:(FlutterError **)error { + NSNumber *playerKey = @(playerIdentifier); + FVPVideoPlayer *player = self.playersByIdentifier[playerKey]; + [self.playersByIdentifier removeObjectForKey:playerKey]; + [player dispose]; } -- (void)setLooping:(BOOL)isLooping forPlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (void)setLooping:(BOOL)isLooping + forPlayer:(NSInteger)playerIdentifier + error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; player.isLooping = isLooping; } -- (void)setVolume:(double)volume forPlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (void)setVolume:(double)volume + forPlayer:(NSInteger)playerIdentifier + error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; [player setVolume:volume]; } -- (void)setPlaybackSpeed:(double)speed forPlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (void)setPlaybackSpeed:(double)speed + forPlayer:(NSInteger)playerIdentifier + error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; [player setPlaybackSpeed:speed]; } -- (void)playPlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (void)playPlayer:(NSInteger)playerIdentifier error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; [player play]; } -- (nullable NSNumber *)positionForPlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (nullable NSNumber *)positionForPlayer:(NSInteger)playerIdentifier error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; return @([player position]); } - (void)seekTo:(NSInteger)position - forPlayer:(NSInteger)textureId + forPlayer:(NSInteger)playerIdentifier completion:(nonnull void (^)(FlutterError *_Nullable))completion { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; [player seekTo:position completionHandler:^(BOOL finished) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -229,8 +302,8 @@ - (void)seekTo:(NSInteger)position }]; } -- (void)pausePlayer:(NSInteger)textureId error:(FlutterError **)error { - FVPVideoPlayer *player = self.playersByTextureId[@(textureId)]; +- (void)pausePlayer:(NSInteger)playerIdentifier error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)]; [player pause]; } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h index 84d928cf230..9170eb64c6a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h @@ -23,4 +23,4 @@ NS_ASSUME_NONNULL_BEGIN @interface FVPDefaultAVFactory : NSObject @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPFrameUpdater.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPFrameUpdater.h index 1e07ca9f019..9d5466d8757 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPFrameUpdater.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPFrameUpdater.h @@ -2,21 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import + #if TARGET_OS_OSX #import #else #import #endif -#import - NS_ASSUME_NONNULL_BEGIN /// FVPFrameUpdater is responsible for notifying the Flutter texture registry /// when a new video frame is available. @interface FVPFrameUpdater : NSObject -/// The texture ID associated with the video output. -@property(nonatomic) int64_t textureId; +/// The texture identifier associated with the video output. +@property(nonatomic) int64_t textureIdentifier; /// The output that this updater is managing. @property(nonatomic, weak) AVPlayerItemVideoOutput *videoOutput; /// The last time that has been validated as avaliable according to hasNewPixelBufferForItemTime:. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoView.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoView.h new file mode 100644 index 00000000000..f05ebccf991 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoView.h @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Platform views are only supported on iOS as of now. Ifdefs are used to avoid compilation errors. + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +/// A class used to create a native video view that can be embedded in a Flutter app. +/// This class wraps an AVPlayer instance and displays its video content. +#if TARGET_OS_IOS +@interface FVPNativeVideoView : NSObject +#else +@interface FVPNativeVideoView : NSView +#endif +/// Initializes a new instance of a native view. +/// It creates a video view instance and sets the provided AVPlayer instance to it. +- (instancetype)initWithPlayer:(AVPlayer *)player; +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoViewFactory.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoViewFactory.h new file mode 100644 index 00000000000..2872bebdfcf --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoViewFactory.h @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Platform views are only supported on iOS as of now. Ifdef is used to avoid compilation errors. + +#import + +#import "FVPVideoPlayer.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +/// A factory class responsible for creating native video views that can be embedded in a +/// Flutter app. +@interface FVPNativeVideoViewFactory : NSObject +/// Initializes a new instance of FVPNativeVideoViewFactory with the given messenger and +/// a block that provides video players associated with their identifiers. +- (instancetype)initWithMessenger:(NSObject *)messenger + playerByIdentifierProvider:(FVPVideoPlayer * (^)(NSNumber *))playerByIdentifierProvider; +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h new file mode 100644 index 00000000000..281438efe8d --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FVPDisplayLink.h" +#import "FVPFrameUpdater.h" +#import "FVPVideoPlayer.h" +#import "FVPVideoPlayer_Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A subclass of FVPVideoPlayer that adds functionality related to texture-based view as a way of +/// displaying the video in the app. It manages the CALayer associated with the Flutter view, +/// updates frames, and handles display link callbacks. +/// If you need to display a video using platform view, use FVPVideoPlayer instead. +@interface FVPTextureBasedVideoPlayer : FVPVideoPlayer +/// Initializes a new instance of FVPTextureBasedVideoPlayer with the given URL, frame updater, +/// display link, HTTP headers, AV factory, and registrar. +- (instancetype)initWithURL:(NSURL *)url + frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink + httpHeaders:(nonnull NSDictionary *)headers + avFactory:(id)avFactory + registrar:(NSObject *)registrar + onDisposed:(void (^)(int64_t))onDisposed; + +/// Initializes a new instance of FVPTextureBasedVideoPlayer with the given asset, frame updater, +/// display link, AV factory, and registrar. +- (instancetype)initWithAsset:(NSString *)asset + frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink + avFactory:(id)avFactory + registrar:(NSObject *)registrar + onDisposed:(void (^)(int64_t))onDisposed; + +/// Sets the texture Identifier for the frame updater. This method should be called once the texture +/// identifier is obtained from the texture registry. +- (void)setTextureIdentifier:(int64_t)textureIdentifier; + +/// Tells the player to run its frame updater until it receives a frame, regardless of the +/// play/pause state. +- (void)expectFrame; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h new file mode 100644 index 00000000000..b751311f5c8 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FVPTextureBasedVideoPlayer.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface FVPTextureBasedVideoPlayer () +/// The AVPlayerLayer used to display the video content. +/// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 +/// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some +/// video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An +/// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams +/// for issue #1, and restore the correct width and height for issue #2. +@property(readonly, nonatomic) AVPlayerLayer *playerLayer; + +/// Called when the texture is unregistered. +/// This method is used to clean up resources associated with the texture. +- (void)onTextureUnregistered:(nullable NSObject *)texture; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h index 27f95e7692d..d668c820065 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h @@ -2,58 +2,47 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import + +#import "FVPAVFactory.h" + #if TARGET_OS_OSX #import #else #import #endif -#import - -#import "FVPAVFactory.h" -#import "FVPDisplayLink.h" -#import "FVPFrameUpdater.h" - NS_ASSUME_NONNULL_BEGIN -/// FVPVideoPlayer is responsible for managing video playback using AVPlayer. -/// It provides methods to control playback, adjust volume, handle seeking, and -/// notify the Flutter engine about new video frames. -@interface FVPVideoPlayer : NSObject +/// FVPVideoPlayer manages video playback using AVPlayer. +/// It provides methods for controlling playback, adjusting volume, and handling seeking. +/// This class contains all functionalities needed to manage video playback in platform views and is +/// typically used alongside FVPNativeVideoViewFactory. If you need to display a video using a +/// texture, use FVPTextureBasedVideoPlayer instead. +@interface FVPVideoPlayer : NSObject /// The Flutter event channel used to communicate with the Flutter engine. @property(nonatomic) FlutterEventChannel *eventChannel; +/// The AVPlayer instance used for video playback. +@property(nonatomic, readonly) AVPlayer *player; /// Indicates whether the video player has been disposed. @property(nonatomic, readonly) BOOL disposed; /// Indicates whether the video player is set to loop. @property(nonatomic) BOOL isLooping; /// The current playback position of the video, in milliseconds. -@property(readonly, nonatomic) int64_t position; +@property(nonatomic, readonly) int64_t position; -/// Initializes a new instance of FVPVideoPlayer with the given asset, frame updater, display link, -/// AV factory, and registrar. +/// Initializes a new instance of FVPVideoPlayer with the given asset, AV factory, and registrar. - (instancetype)initWithAsset:(NSString *)asset - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink avFactory:(id)avFactory registrar:(NSObject *)registrar; -/// Initializes a new instance of FVPVideoPlayer with the given URL, frame updater, display link, -/// HTTP headers, AV factory, and registrar. +/// Initializes a new instance of FVPVideoPlayer with the given URL, HTTP headers, AV factory, and +/// registrar. - (instancetype)initWithURL:(NSURL *)url - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink httpHeaders:(nonnull NSDictionary *)headers avFactory:(id)avFactory registrar:(NSObject *)registrar; -/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, frame updater, display -/// link, AV factory, and registrar. -- (instancetype)initWithPlayerItem:(AVPlayerItem *)item - frameUpdater:(FVPFrameUpdater *)frameUpdater - displayLink:(FVPDisplayLink *)displayLink - avFactory:(id)avFactory - registrar:(NSObject *)registrar; - /// Disposes the video player and releases any resources it holds. - (void)dispose; @@ -77,10 +66,6 @@ NS_ASSUME_NONNULL_BEGIN /// Seeks to the specified location in the video and calls the completion handler when done, if one /// is supplied. - (void)seekTo:(int64_t)location completionHandler:(void (^_Nullable)(BOOL))completionHandler; - -/// Tells the player to run its frame updater until it receives a frame, regardless of the -/// play/pause state. -- (void)expectFrame; @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h index e045210e68c..0c35429d026 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h @@ -19,7 +19,7 @@ @interface FVPVideoPlayerPlugin () @property(readonly, strong, nonatomic) - NSMutableDictionary *playersByTextureId; + NSMutableDictionary *playersByIdentifier; - (instancetype)initWithAVFactory:(id)avFactory displayLinkFactory:(id)displayLinkFactory diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h new file mode 100644 index 00000000000..995952de17e --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import "FVPAVFactory.h" +#import "FVPVideoPlayer.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +/// Interface intended for use by subclasses, but not other callers. +@interface FVPVideoPlayer () +/// The AVPlayerItemVideoOutput associated with this video player. +@property(nonatomic, readonly) AVPlayerItemVideoOutput *videoOutput; +/// The plugin registrar, to obtain view information from. +@property(nonatomic, readonly) NSObject *registrar; +/// The Flutter event sink used to send events to the Flutter engine. +@property(nonatomic) FlutterEventSink eventSink; +/// The preferred transform for the video. It can be used to handle the rotation of the video. +@property(nonatomic) CGAffineTransform preferredTransform; +/// The target playback speed requested by the plugin client. +@property(nonatomic, readonly) NSNumber *targetPlaybackSpeed; +/// Indicates whether the video player is currently playing. +@property(nonatomic, readonly) BOOL isPlaying; +/// Indicates whether the video player has been initialized. +@property(nonatomic, readonly) BOOL isInitialized; + +/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, frame updater, display +/// link, AV factory, and registrar. +- (instancetype)initWithPlayerItem:(AVPlayerItem *)item + avFactory:(id)avFactory + registrar:(NSObject *)registrar; + +/// Updates the playing state of the video player. +- (void)updatePlayingState; + +/// Returns the absolute file path for a given asset name. +/// This method attempts to locate the specified asset within the app bundle. ++ (NSString *)absolutePathForAssetName:(NSString *)assetName; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Test.h index d51d7a1a1e0..967855d85bd 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Test.h @@ -2,30 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import "FVPVideoPlayer.h" + #if TARGET_OS_OSX #import #else #import #endif -#import "FVPVideoPlayer.h" - NS_ASSUME_NONNULL_BEGIN -@interface FVPVideoPlayer () -/// The AVPlayer instance used for video playback. -@property(readonly, nonatomic) AVPlayer *player; -/// The AVPlayerLayer used to display the video content. -/// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 -/// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some -/// video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An -/// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams -/// for issue #1, and restore the correct width and height for issue #2. -@property(readonly, nonatomic) AVPlayerLayer *playerLayer; - -/// Called when the texture is unregistered. -/// This method is used to clean up resources associated with the texture. -- (void)onTextureUnregistered:(nullable NSObject *)texture; +@interface FVPVideoPlayer () @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index afb118f14cd..6a115c6b9ac 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -13,8 +13,29 @@ NS_ASSUME_NONNULL_BEGIN +/// Pigeon equivalent of VideoViewType. +typedef NS_ENUM(NSUInteger, FVPPlatformVideoViewType) { + FVPPlatformVideoViewTypeTextureView = 0, + FVPPlatformVideoViewTypePlatformView = 1, +}; + +/// Wrapper for FVPPlatformVideoViewType to allow for nullability. +@interface FVPPlatformVideoViewTypeBox : NSObject +@property(nonatomic, assign) FVPPlatformVideoViewType value; +- (instancetype)initWithValue:(FVPPlatformVideoViewType)value; +@end + +@class FVPPlatformVideoViewCreationParams; @class FVPCreationOptions; +/// Information passed to the platform view creation. +@interface FVPPlatformVideoViewCreationParams : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId; +@property(nonatomic, assign) NSInteger playerId; +@end + @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -22,12 +43,14 @@ NS_ASSUME_NONNULL_BEGIN uri:(nullable NSString *)uri packageName:(nullable NSString *)packageName formatHint:(nullable NSString *)formatHint - httpHeaders:(NSDictionary *)httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders + viewType:(FVPPlatformVideoViewType)viewType; @property(nonatomic, copy, nullable) NSString *asset; @property(nonatomic, copy, nullable) NSString *uri; @property(nonatomic, copy, nullable) NSString *packageName; @property(nonatomic, copy, nullable) NSString *formatHint; @property(nonatomic, copy) NSDictionary *httpHeaders; +@property(nonatomic, assign) FVPPlatformVideoViewType viewType; @end /// The codec used by all APIs. @@ -38,24 +61,24 @@ NSObject *FVPGetMessagesCodec(void); /// @return `nil` only when `error != nil`. - (nullable NSNumber *)createWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; -- (void)disposePlayer:(NSInteger)textureId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)disposePlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; - (void)setLooping:(BOOL)isLooping - forPlayer:(NSInteger)textureId + forPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; - (void)setVolume:(double)volume - forPlayer:(NSInteger)textureId + forPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; - (void)setPlaybackSpeed:(double)speed - forPlayer:(NSInteger)textureId + forPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; -- (void)playPlayer:(NSInteger)textureId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)playPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)positionForPlayer:(NSInteger)textureId +- (nullable NSNumber *)positionForPlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; - (void)seekTo:(NSInteger)position - forPlayer:(NSInteger)textureId + forPlayer:(NSInteger)playerId completion:(void (^)(FlutterError *_Nullable))completion; -- (void)pausePlayer:(NSInteger)textureId error:(FlutterError *_Nullable *_Nonnull)error; +- (void)pausePlayer:(NSInteger)playerId error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 9749b125a9e..83c41211f74 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/video_player_avfoundation/messages.g.h" @@ -30,24 +30,66 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { return (result == [NSNull null]) ? nil : result; } +/// Pigeon equivalent of VideoViewType. +@implementation FVPPlatformVideoViewTypeBox +- (instancetype)initWithValue:(FVPPlatformVideoViewType)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + +@interface FVPPlatformVideoViewCreationParams () ++ (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list; ++ (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FVPCreationOptions () + (FVPCreationOptions *)fromList:(NSArray *)list; + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@implementation FVPPlatformVideoViewCreationParams ++ (instancetype)makeWithPlayerId:(NSInteger)playerId { + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; + pigeonResult.playerId = playerId; + return pigeonResult; +} ++ (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; + pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray *)list { + return (list) ? [FVPPlatformVideoViewCreationParams fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.playerId), + ]; +} +@end + @implementation FVPCreationOptions + (instancetype)makeWithAsset:(nullable NSString *)asset uri:(nullable NSString *)uri packageName:(nullable NSString *)packageName formatHint:(nullable NSString *)formatHint - httpHeaders:(NSDictionary *)httpHeaders { + httpHeaders:(NSDictionary *)httpHeaders + viewType:(FVPPlatformVideoViewType)viewType { FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.asset = asset; pigeonResult.uri = uri; pigeonResult.packageName = packageName; pigeonResult.formatHint = formatHint; pigeonResult.httpHeaders = httpHeaders; + pigeonResult.viewType = viewType; return pigeonResult; } + (FVPCreationOptions *)fromList:(NSArray *)list { @@ -57,6 +99,8 @@ + (FVPCreationOptions *)fromList:(NSArray *)list { pigeonResult.packageName = GetNullableObjectAtIndex(list, 2); pigeonResult.formatHint = GetNullableObjectAtIndex(list, 3); pigeonResult.httpHeaders = GetNullableObjectAtIndex(list, 4); + FVPPlatformVideoViewTypeBox *boxedFVPPlatformVideoViewType = GetNullableObjectAtIndex(list, 5); + pigeonResult.viewType = boxedFVPPlatformVideoViewType.value; return pigeonResult; } + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @@ -69,6 +113,7 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { self.packageName ?: [NSNull null], self.formatHint ?: [NSNull null], self.httpHeaders ?: [NSNull null], + [[FVPPlatformVideoViewTypeBox alloc] initWithValue:self.viewType], ]; } @end @@ -78,7 +123,15 @@ @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil + ? nil + : [[FVPPlatformVideoViewTypeBox alloc] initWithValue:[enumAsNumber integerValue]]; + } + case 130: + return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; + case 131: return [FVPCreationOptions fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -90,8 +143,15 @@ @interface FVPMessagesPigeonCodecWriter : FlutterStandardWriter @end @implementation FVPMessagesPigeonCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[FVPCreationOptions class]]) { + if ([value isKindOfClass:[FVPPlatformVideoViewTypeBox class]]) { + FVPPlatformVideoViewTypeBox *box = (FVPPlatformVideoViewTypeBox *)value; [self writeByte:129]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FVPPlatformVideoViewCreationParams class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPCreationOptions class]]) { + [self writeByte:131]; [self writeValue:[value toList]]; } else { [super writeValue:value]; @@ -191,9 +251,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; FlutterError *error; - [api disposePlayer:arg_textureId error:&error]; + [api disposePlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -216,9 +276,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_isLooping = [GetNullableObjectAtIndex(args, 0) boolValue]; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 1) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 1) integerValue]; FlutterError *error; - [api setLooping:arg_isLooping forPlayer:arg_textureId error:&error]; + [api setLooping:arg_isLooping forPlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -241,9 +301,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 1) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 1) integerValue]; FlutterError *error; - [api setVolume:arg_volume forPlayer:arg_textureId error:&error]; + [api setVolume:arg_volume forPlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -266,9 +326,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 1) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 1) integerValue]; FlutterError *error; - [api setPlaybackSpeed:arg_speed forPlayer:arg_textureId error:&error]; + [api setPlaybackSpeed:arg_speed forPlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -290,9 +350,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; FlutterError *error; - [api playPlayer:arg_textureId error:&error]; + [api playPlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -314,9 +374,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; FlutterError *error; - NSNumber *output = [api positionForPlayer:arg_textureId error:&error]; + NSNumber *output = [api positionForPlayer:arg_playerId error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -339,9 +399,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 1) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 1) integerValue]; [api seekTo:arg_position - forPlayer:arg_textureId + forPlayer:arg_playerId completion:^(FlutterError *_Nullable error) { callback(wrapResult(nil, error)); }]; @@ -365,9 +425,9 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - NSInteger arg_textureId = [GetNullableObjectAtIndex(args, 0) integerValue]; + NSInteger arg_playerId = [GetNullableObjectAtIndex(args, 0) integerValue]; FlutterError *error; - [api pausePlayer:arg_textureId error:&error]; + [api pausePlayer:arg_playerId error:&error]; callback(wrapResult(nil, error)); }]; } else { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoView.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoView.m new file mode 100644 index 00000000000..f00791582e9 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoView.m @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "../video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoView.h" + +#import + +@interface FVPPlayerView : UIView +@end + +@implementation FVPPlayerView ++ (Class)layerClass { + return [AVPlayerLayer class]; +} + +- (void)setPlayer:(AVPlayer *)player { + [(AVPlayerLayer *)[self layer] setPlayer:player]; +} +@end + +@interface FVPNativeVideoView () +@property(nonatomic) FVPPlayerView *playerView; +@end + +@implementation FVPNativeVideoView +- (instancetype)initWithPlayer:(AVPlayer *)player { + if (self = [super init]) { + _playerView = [[FVPPlayerView alloc] init]; + [_playerView setPlayer:player]; + } + return self; +} + +- (FVPPlayerView *)view { + return self.playerView; +} +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoViewFactory.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoViewFactory.m new file mode 100644 index 00000000000..94ece09e11e --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_ios/FVPNativeVideoViewFactory.m @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "../video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoViewFactory.h" + +#import "../video_player_avfoundation/include/video_player_avfoundation/FVPNativeVideoView.h" +#import "../video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h" +#import "../video_player_avfoundation/include/video_player_avfoundation/messages.g.h" + +@interface FVPNativeVideoViewFactory () +@property(nonatomic, strong) NSObject *messenger; +@property(nonatomic, copy) FVPVideoPlayer * (^playerByIdProvider)(NSNumber *); +@end + +@implementation FVPNativeVideoViewFactory + +- (instancetype)initWithMessenger:(NSObject *)messenger + playerByIdentifierProvider:(FVPVideoPlayer * (^)(NSNumber *))playerByIdProvider { + self = [super init]; + if (self) { + _messenger = messenger; + _playerByIdProvider = [playerByIdProvider copy]; + } + return self; +} + +- (NSObject *)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewIdentifier + arguments:(FVPPlatformVideoViewCreationParams *)args { + NSNumber *playerIdentifier = @(args.playerId); + FVPVideoPlayer *player = self.playerByIdProvider(playerIdentifier); + return [[FVPNativeVideoView alloc] initWithPlayer:player.player]; +} + +- (NSObject *)createArgsCodec { + return FVPGetMessagesCodec(); +} + +@end diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 94ce699b626..e8166db9ca2 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -220,6 +220,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 39F8523A410339490F826122 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -350,6 +351,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 39F8523A410339490F826122 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/video_player_avfoundation/video_player_avfoundation_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/video_player_avfoundation_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 54c97030c3a..cebe79bde3f 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -21,30 +21,39 @@ - (void)setUp { - (void)testPlayVideo { XCUIApplication *app = self.app; - - XCUIElement *remoteTab = [app.otherElements - elementMatchingPredicate:[NSPredicate predicateWithFormat:@"selected == YES"]]; + XCUIElement *remoteTab = + [[app.otherElements matchingPredicate:[NSPredicate predicateWithFormat:@"selected == YES"]] + elementBoundByIndex:0]; XCTAssertTrue([remoteTab waitForExistenceWithTimeout:30.0]); XCTAssertTrue([remoteTab.label containsString:@"Remote"]); - XCUIElement *playButton = app.staticTexts[@"Play"]; - XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); - [playButton tap]; + // Go through both platform view and texture view. + for (NSString *tabName in @[ @"Platform view", @"Texture view" ]) { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; + XCUIElement *viewTypeTab = [app.staticTexts elementMatchingPredicate:predicate]; + XCTAssertTrue([viewTypeTab waitForExistenceWithTimeout:30.0]); + XCTAssertFalse(viewTypeTab.isSelected); + [viewTypeTab tap]; + + XCUIElement *playButton = app.staticTexts[@"Play"]; + XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); + [playButton tap]; - NSPredicate *find1xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '1.0x'"]; - XCUIElement *playbackSpeed1x = [app.staticTexts elementMatchingPredicate:find1xButton]; - BOOL foundPlaybackSpeed1x = [playbackSpeed1x waitForExistenceWithTimeout:30.0]; - XCTAssertTrue(foundPlaybackSpeed1x); - [playbackSpeed1x tap]; + NSPredicate *find1xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '1.0x'"]; + XCUIElement *playbackSpeed1x = [app.staticTexts elementMatchingPredicate:find1xButton]; + BOOL foundPlaybackSpeed1x = [playbackSpeed1x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed1x); + [playbackSpeed1x tap]; - XCUIElement *playbackSpeed5xButton = app.buttons[@"5.0x"]; - XCTAssertTrue([playbackSpeed5xButton waitForExistenceWithTimeout:30.0]); - [playbackSpeed5xButton tap]; + XCUIElement *playbackSpeed5xButton = app.buttons[@"5.0x"]; + XCTAssertTrue([playbackSpeed5xButton waitForExistenceWithTimeout:30.0]); + [playbackSpeed5xButton tap]; - NSPredicate *find5xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '5.0x'"]; - XCUIElement *playbackSpeed5x = [app.staticTexts elementMatchingPredicate:find5xButton]; - BOOL foundPlaybackSpeed5x = [playbackSpeed5x waitForExistenceWithTimeout:30.0]; - XCTAssertTrue(foundPlaybackSpeed5x); + NSPredicate *find5xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '5.0x'"]; + XCUIElement *playbackSpeed5x = [app.staticTexts elementMatchingPredicate:find5xButton]; + BOOL foundPlaybackSpeed5x = [playbackSpeed5x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed5x); + } // Cycle through tabs. for (NSString *tabName in @[ @"Asset mp4", @"Remote mp4" ]) { diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index 313ae7f50dc..0067b639c96 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -4,7 +4,10 @@ // ignore_for_file: public_member_api_docs +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'mini_controller.dart'; @@ -41,18 +44,97 @@ class _App extends StatelessWidget { ), ), body: TabBarView( - children: [ - _BumbleBeeRemoteVideo(), - _BumbleBeeEncryptedLiveStream(), - _ButterFlyAssetVideo(), - ], + children: Platform.isIOS + ? [ + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _BumbleBeeRemoteVideo(viewType), + ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _BumbleBeeEncryptedLiveStream(viewType), + ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _ButterFlyAssetVideo(viewType), + ), + ] + // Platform views are only supported on iOS as of now. + : const [ + _BumbleBeeRemoteVideo(VideoViewType.textureView), + _BumbleBeeEncryptedLiveStream(VideoViewType.textureView), + _ButterFlyAssetVideo(VideoViewType.textureView), + ], ), ), ); } } +class _ViewTypeTabBar extends StatefulWidget { + const _ViewTypeTabBar({ + required this.builder, + }); + + final Widget Function(VideoViewType) builder; + + @override + State<_ViewTypeTabBar> createState() => _ViewTypeTabBarState(); +} + +class _ViewTypeTabBarState extends State<_ViewTypeTabBar> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TabBar( + controller: _tabController, + isScrollable: true, + tabs: const [ + Tab( + icon: Icon(Icons.texture), + text: 'Texture view', + ), + Tab( + icon: Icon(Icons.construction), + text: 'Platform view', + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + widget.builder(VideoViewType.textureView), + widget.builder(VideoViewType.platformView), + ], + ), + ), + ], + ); + } +} + class _ButterFlyAssetVideo extends StatefulWidget { + const _ButterFlyAssetVideo(this.viewType); + + final VideoViewType viewType; + @override _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState(); } @@ -63,7 +145,10 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { @override void initState() { super.initState(); - _controller = MiniController.asset('assets/Butterfly-209.mp4'); + _controller = MiniController.asset( + 'assets/Butterfly-209.mp4', + viewType: widget.viewType, + ); _controller.addListener(() { setState(() {}); @@ -108,6 +193,10 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { } class _BumbleBeeRemoteVideo extends StatefulWidget { + const _BumbleBeeRemoteVideo(this.viewType); + + final VideoViewType viewType; + @override _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState(); } @@ -120,6 +209,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { super.initState(); _controller = MiniController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + viewType: widget.viewType, ); _controller.addListener(() { @@ -162,6 +252,10 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } class _BumbleBeeEncryptedLiveStream extends StatefulWidget { + const _BumbleBeeEncryptedLiveStream(this.viewType); + + final VideoViewType viewType; + @override _BumbleBeeEncryptedLiveStreamState createState() => _BumbleBeeEncryptedLiveStreamState(); @@ -176,6 +270,7 @@ class _BumbleBeeEncryptedLiveStreamState super.initState(); _controller = MiniController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8', + viewType: widget.viewType, ); _controller.addListener(() { diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index a38013533d6..7d406ecbd1a 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -167,20 +167,27 @@ class MiniController extends ValueNotifier { /// The name of the asset is given by the [dataSource] argument and must not be /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. - MiniController.asset(this.dataSource, {this.package}) - : dataSourceType = DataSourceType.asset, + MiniController.asset( + this.dataSource, { + this.package, + this.viewType = VideoViewType.textureView, + }) : dataSourceType = DataSourceType.asset, super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from /// the network. - MiniController.network(this.dataSource) - : dataSourceType = DataSourceType.network, + MiniController.network( + this.dataSource, { + this.viewType = VideoViewType.textureView, + }) : dataSourceType = DataSourceType.network, package = null, super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from a file. - MiniController.file(File file) - : dataSource = Uri.file(file.absolute.path).toString(), + MiniController.file( + File file, { + this.viewType = VideoViewType.textureView, + }) : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, super(const VideoPlayerValue(duration: Duration.zero)); @@ -196,19 +203,22 @@ class MiniController extends ValueNotifier { /// Only set for [asset] videos. The package that the asset was loaded from. final String? package; + /// The type of view used to display the video. + final VideoViewType viewType; + Timer? _timer; Completer? _creatingCompleter; StreamSubscription? _eventSubscription; - /// The id of a texture that hasn't been initialized. + /// The id of a player that hasn't been initialized. @visibleForTesting - static const int kUninitializedTextureId = -1; - int _textureId = kUninitializedTextureId; + static const int kUninitializedPlayerId = -1; + int _playerId = kUninitializedPlayerId; /// This is just exposed for testing. It shouldn't be used by anyone depending /// on the plugin. @visibleForTesting - int get textureId => _textureId; + int get playerId => _playerId; /// Attempts to open the given [dataSource] and load metadata about the video. Future initialize() async { @@ -239,8 +249,13 @@ class MiniController extends ValueNotifier { ); } - _textureId = (await _platform.create(dataSourceDescription)) ?? - kUninitializedTextureId; + final VideoCreationOptions creationOptions = VideoCreationOptions( + dataSource: dataSourceDescription, + viewType: viewType, + ); + + _playerId = (await _platform.createWithOptions(creationOptions)) ?? + kUninitializedPlayerId; _creatingCompleter!.complete(null); final Completer initializingCompleter = Completer(); @@ -253,8 +268,8 @@ class MiniController extends ValueNotifier { isInitialized: event.duration != null, ); initializingCompleter.complete(null); - _platform.setVolume(_textureId, 1.0); - _platform.setLooping(_textureId, true); + _platform.setVolume(_playerId, 1.0); + _platform.setLooping(_playerId, true); _applyPlayPause(); case VideoEventType.completed: pause().then((void pauseResult) => seekTo(value.duration)); @@ -281,7 +296,7 @@ class MiniController extends ValueNotifier { } _eventSubscription = _platform - .videoEventsFor(_textureId) + .videoEventsFor(_playerId) .listen(eventListener, onError: errorListener); return initializingCompleter.future; } @@ -292,7 +307,7 @@ class MiniController extends ValueNotifier { await _creatingCompleter!.future; _timer?.cancel(); await _eventSubscription?.cancel(); - await _platform.dispose(_textureId); + await _platform.dispose(_playerId); } super.dispose(); } @@ -312,7 +327,7 @@ class MiniController extends ValueNotifier { Future _applyPlayPause() async { _timer?.cancel(); if (value.isPlaying) { - await _platform.play(_textureId); + await _platform.play(_playerId); _timer = Timer.periodic( const Duration(milliseconds: 500), @@ -326,14 +341,14 @@ class MiniController extends ValueNotifier { ); await _applyPlaybackSpeed(); } else { - await _platform.pause(_textureId); + await _platform.pause(_playerId); } } Future _applyPlaybackSpeed() async { if (value.isPlaying) { await _platform.setPlaybackSpeed( - _textureId, + _playerId, value.playbackSpeed, ); } @@ -341,7 +356,7 @@ class MiniController extends ValueNotifier { /// The position in the current video. Future get position async { - return _platform.getPosition(_textureId); + return _platform.getPosition(_playerId); } /// Sets the video's current timestamp to be at [position]. @@ -351,7 +366,7 @@ class MiniController extends ValueNotifier { } else if (position < Duration.zero) { position = Duration.zero; } - await _platform.seekTo(_textureId, position); + await _platform.seekTo(_playerId, position); _updatePosition(position); } @@ -382,10 +397,10 @@ class VideoPlayer extends StatefulWidget { class _VideoPlayerState extends State { _VideoPlayerState() { _listener = () { - final int newTextureId = widget.controller.textureId; - if (newTextureId != _textureId) { + final int newPlayerId = widget.controller.playerId; + if (newPlayerId != _playerId) { setState(() { - _textureId = newTextureId; + _playerId = newPlayerId; }); } }; @@ -393,13 +408,13 @@ class _VideoPlayerState extends State { late VoidCallback _listener; - late int _textureId; + late int _playerId; @override void initState() { super.initState(); - _textureId = widget.controller.textureId; - // Need to listen for initialization events since the actual texture ID + _playerId = widget.controller.playerId; + // Need to listen for initialization events since the actual player ID // becomes available after asynchronous initialization finishes. widget.controller.addListener(_listener); } @@ -408,7 +423,7 @@ class _VideoPlayerState extends State { void didUpdateWidget(VideoPlayer oldWidget) { super.didUpdateWidget(oldWidget); oldWidget.controller.removeListener(_listener); - _textureId = widget.controller.textureId; + _playerId = widget.controller.playerId; widget.controller.addListener(_listener); } @@ -420,9 +435,11 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == MiniController.kUninitializedTextureId + return _playerId == MiniController.kUninitializedPlayerId ? Container() - : _platform.buildView(_textureId); + : _platform.buildViewWithOptions( + VideoViewOptions(playerId: _playerId), + ); } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index ab457872a0d..9441576e59d 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ">=6.1.0 <7.0.0" + video_player_platform_interface: ^6.3.0 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 584d4730731..ec790187abf 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -4,20 +4,24 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'messages.g.dart'; -// TODO(FirentisTFW): Remove the ignore and rename parameters when adding support for platform views. -// ignore_for_file: avoid_renaming_method_parameters - /// An iOS implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerApi]. class AVFoundationVideoPlayer extends VideoPlayerPlatform { final AVFoundationVideoPlayerApi _api = AVFoundationVideoPlayerApi(); + /// A map that associates player ID with a view state. + /// This is used to determine which view type to use when building a view. + @visibleForTesting + final Map playerViewStates = + {}; + /// Registers this class as the default instance of [VideoPlayerPlatform]. static void registerWith() { VideoPlayerPlatform.instance = AVFoundationVideoPlayer(); @@ -29,12 +33,31 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { } @override - Future dispose(int textureId) { - return _api.dispose(textureId); + Future dispose(int playerId) async { + await _api.dispose(playerId); + playerViewStates.remove(playerId); } @override Future create(DataSource dataSource) async { + return createWithOptions( + VideoCreationOptions( + dataSource: dataSource, + // Texture view was the only supported view type before + // createWithOptions was introduced. + viewType: VideoViewType.textureView, + ), + ); + } + + @override + Future createWithOptions(VideoCreationOptions options) async { + final DataSource dataSource = options.dataSource; + // Platform views are not supported on macOS yet. Use texture view instead. + final VideoViewType viewType = defaultTargetPlatform == TargetPlatform.macOS + ? VideoViewType.textureView + : options.viewType; + String? asset; String? packageName; String? uri; @@ -53,58 +76,67 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { case DataSourceType.contentUri: uri = dataSource.uri; } - final CreationOptions options = CreationOptions( + final CreationOptions pigeonCreationOptions = CreationOptions( asset: asset, packageName: packageName, uri: uri, httpHeaders: httpHeaders, formatHint: formatHint, + viewType: _platformVideoViewTypeFromVideoViewType(viewType), ); - return _api.create(options); + final int playerId = await _api.create(pigeonCreationOptions); + playerViewStates[playerId] = switch (viewType) { + // playerId is also the textureId when using texture view. + VideoViewType.textureView => + VideoPlayerTextureViewState(textureId: playerId), + VideoViewType.platformView => const VideoPlayerPlatformViewState(), + }; + + return playerId; } @override - Future setLooping(int textureId, bool looping) { - return _api.setLooping(looping, textureId); + Future setLooping(int playerId, bool looping) { + return _api.setLooping(looping, playerId); } @override - Future play(int textureId) { - return _api.play(textureId); + Future play(int playerId) { + return _api.play(playerId); } @override - Future pause(int textureId) { - return _api.pause(textureId); + Future pause(int playerId) { + return _api.pause(playerId); } @override - Future setVolume(int textureId, double volume) { - return _api.setVolume(volume, textureId); + Future setVolume(int playerId, double volume) { + return _api.setVolume(volume, playerId); } @override - Future setPlaybackSpeed(int textureId, double speed) { + Future setPlaybackSpeed(int playerId, double speed) { assert(speed > 0); - return _api.setPlaybackSpeed(speed, textureId); + return _api.setPlaybackSpeed(speed, playerId); } @override - Future seekTo(int textureId, Duration position) { - return _api.seekTo(position.inMilliseconds, textureId); + Future seekTo(int playerId, Duration position) { + return _api.seekTo(position.inMilliseconds, playerId); } @override - Future getPosition(int textureId) async { - final int position = await _api.getPosition(textureId); + Future getPosition(int playerId) async { + final int position = await _api.getPosition(playerId); return Duration(milliseconds: position); } @override - Stream videoEventsFor(int textureId) { - return _eventChannelFor(textureId) + Stream videoEventsFor(int playerId) { + return _eventChannelFor(playerId) .receiveBroadcastStream() .map((dynamic event) { final Map map = event as Map; @@ -143,17 +175,48 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { } @override - Widget buildView(int textureId) { - return Texture(textureId: textureId); + Future setMixWithOthers(bool mixWithOthers) { + return _api.setMixWithOthers(mixWithOthers); } @override - Future setMixWithOthers(bool mixWithOthers) { - return _api.setMixWithOthers(mixWithOthers); + Widget buildView(int playerId) { + return buildViewWithOptions( + VideoViewOptions(playerId: playerId), + ); } - EventChannel _eventChannelFor(int textureId) { - return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); + @override + Widget buildViewWithOptions(VideoViewOptions options) { + final int playerId = options.playerId; + final VideoPlayerViewState? viewState = playerViewStates[playerId]; + + return switch (viewState) { + VideoPlayerTextureViewState(:final int textureId) => + Texture(textureId: textureId), + VideoPlayerPlatformViewState() => _buildPlatformView(playerId), + null => throw Exception( + 'Could not find corresponding view type for playerId: $playerId', + ), + }; + } + + Widget _buildPlatformView(int playerId) { + final PlatformVideoViewCreationParams creationParams = + PlatformVideoViewCreationParams(playerId: playerId); + + return IgnorePointer( + // IgnorePointer so that GestureDetector can be used above the platform view. + child: UiKitView( + viewType: 'plugins.flutter.dev/video_player_ios', + creationParams: creationParams, + creationParamsCodec: AVFoundationVideoPlayerApi.pigeonChannelCodec, + ), + ); + } + + EventChannel _eventChannelFor(int playerId) { + return EventChannel('flutter.io/videoPlayer/videoEvents$playerId'); } static const Map _videoFormatStringMap = @@ -172,3 +235,46 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { ); } } + +PlatformVideoViewType _platformVideoViewTypeFromVideoViewType( + VideoViewType viewType, +) { + return switch (viewType) { + VideoViewType.textureView => PlatformVideoViewType.textureView, + VideoViewType.platformView => PlatformVideoViewType.platformView, + }; +} + +/// Base class representing the state of a video player view. +@visibleForTesting +@immutable +sealed class VideoPlayerViewState { + const VideoPlayerViewState(); +} + +/// Represents the state of a video player view that uses a texture. +@visibleForTesting +final class VideoPlayerTextureViewState extends VideoPlayerViewState { + /// Creates a new instance of [VideoPlayerTextureViewState]. + const VideoPlayerTextureViewState({ + required this.textureId, + }); + + /// The ID of the texture used by the video player. + final int textureId; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoPlayerTextureViewState && other.textureId == textureId; + + @override + int get hashCode => textureId.hashCode; +} + +/// Represents the state of a video player view that uses a platform view. +@visibleForTesting +final class VideoPlayerPlatformViewState extends VideoPlayerViewState { + /// Creates a new instance of [VideoPlayerPlatformViewState]. + const VideoPlayerPlatformViewState(); +} diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index ea4a93fdaba..c1d16ecce5e 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -29,6 +29,34 @@ List wrapResponse( return [error.code, error.message, error.details]; } +/// Pigeon equivalent of VideoViewType. +enum PlatformVideoViewType { + textureView, + platformView, +} + +/// Information passed to the platform view creation. +class PlatformVideoViewCreationParams { + PlatformVideoViewCreationParams({ + required this.playerId, + }); + + int playerId; + + Object encode() { + return [ + playerId, + ]; + } + + static PlatformVideoViewCreationParams decode(Object result) { + result as List; + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); + } +} + class CreationOptions { CreationOptions({ this.asset, @@ -36,6 +64,7 @@ class CreationOptions { this.packageName, this.formatHint, required this.httpHeaders, + required this.viewType, }); String? asset; @@ -48,6 +77,8 @@ class CreationOptions { Map httpHeaders; + PlatformVideoViewType viewType; + Object encode() { return [ asset, @@ -55,6 +86,7 @@ class CreationOptions { packageName, formatHint, httpHeaders, + viewType, ]; } @@ -67,6 +99,7 @@ class CreationOptions { formatHint: result[3] as String?, httpHeaders: (result[4] as Map?)!.cast(), + viewType: result[5]! as PlatformVideoViewType, ); } } @@ -78,8 +111,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is CreationOptions) { + } else if (value is PlatformVideoViewType) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is CreationOptions) { + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -90,6 +129,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoViewType.values[value]; + case 130: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 131: return CreationOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -165,7 +209,7 @@ class AVFoundationVideoPlayerApi { } } - Future dispose(int textureId) async { + Future dispose(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -175,7 +219,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -189,7 +233,7 @@ class AVFoundationVideoPlayerApi { } } - Future setLooping(bool isLooping, int textureId) async { + Future setLooping(bool isLooping, int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setLooping$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -199,7 +243,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([isLooping, textureId]) as List?; + .send([isLooping, playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -213,7 +257,7 @@ class AVFoundationVideoPlayerApi { } } - Future setVolume(double volume, int textureId) async { + Future setVolume(double volume, int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setVolume$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -223,7 +267,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([volume, textureId]) as List?; + .send([volume, playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -237,7 +281,7 @@ class AVFoundationVideoPlayerApi { } } - Future setPlaybackSpeed(double speed, int textureId) async { + Future setPlaybackSpeed(double speed, int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -247,7 +291,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([speed, textureId]) as List?; + .send([speed, playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -261,7 +305,7 @@ class AVFoundationVideoPlayerApi { } } - Future play(int textureId) async { + Future play(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.play$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -271,7 +315,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -285,7 +329,7 @@ class AVFoundationVideoPlayerApi { } } - Future getPosition(int textureId) async { + Future getPosition(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getPosition$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -295,7 +339,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -314,7 +358,7 @@ class AVFoundationVideoPlayerApi { } } - Future seekTo(int position, int textureId) async { + Future seekTo(int position, int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.seekTo$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -324,7 +368,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([position, textureId]) as List?; + .send([position, playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -338,7 +382,7 @@ class AVFoundationVideoPlayerApi { } } - Future pause(int textureId) async { + Future pause(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.pause$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -348,7 +392,7 @@ class AVFoundationVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 7a2d2957be1..ffa2f968c32 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -17,13 +17,34 @@ import 'package:pigeon/pigeon.dart'; ), copyrightHeader: 'pigeons/copyright.txt', )) + +/// Pigeon equivalent of VideoViewType. +enum PlatformVideoViewType { + textureView, + platformView, +} + +/// Information passed to the platform view creation. +class PlatformVideoViewCreationParams { + const PlatformVideoViewCreationParams({ + required this.playerId, + }); + + final int playerId; +} + class CreationOptions { - CreationOptions({required this.httpHeaders}); + CreationOptions({ + required this.httpHeaders, + required this.viewType, + }); + String? asset; String? uri; String? packageName; String? formatHint; Map httpHeaders; + PlatformVideoViewType viewType; } @HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') @@ -34,22 +55,22 @@ abstract class AVFoundationVideoPlayerApi { // Creates a new player and returns its ID. int create(CreationOptions creationOptions); @ObjCSelector('disposePlayer:') - void dispose(int textureId); + void dispose(int playerId); @ObjCSelector('setLooping:forPlayer:') - void setLooping(bool isLooping, int textureId); + void setLooping(bool isLooping, int playerId); @ObjCSelector('setVolume:forPlayer:') - void setVolume(double volume, int textureId); + void setVolume(double volume, int playerId); @ObjCSelector('setPlaybackSpeed:forPlayer:') - void setPlaybackSpeed(double speed, int textureId); + void setPlaybackSpeed(double speed, int playerId); @ObjCSelector('playPlayer:') - void play(int textureId); + void play(int playerId); @ObjCSelector('positionForPlayer:') - int getPosition(int textureId); + int getPosition(int playerId); @async @ObjCSelector('seekTo:forPlayer:') - void seekTo(int position, int textureId); + void seekTo(int position, int playerId); @ObjCSelector('pausePlayer:') - void pause(int textureId); + void pause(int playerId); @ObjCSelector('setMixWithOthers:') void setMixWithOthers(bool mixWithOthers); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index c7f9dadb777..3db089c7ccf 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.6.7 +version: 2.7.0 environment: sdk: ^3.4.0 @@ -24,7 +24,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=6.1.0 <7.0.0" + video_player_platform_interface: ^6.3.0 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index dac0553fbed..3d02cef3cbe 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player_avfoundation/src/messages.g.dart'; @@ -12,7 +13,7 @@ import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; - int? textureId; + int? playerId; CreationOptions? creationOptions; int? position; bool? looping; @@ -28,9 +29,9 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - void dispose(int textureId) { + void dispose(int playerId) { log.add('dispose'); - this.textureId = textureId; + this.playerId = playerId; } @override @@ -39,15 +40,15 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - void pause(int textureId) { + void pause(int playerId) { log.add('pause'); - this.textureId = textureId; + this.playerId = playerId; } @override - void play(int textureId) { + void play(int playerId) { log.add('play'); - this.textureId = textureId; + this.playerId = playerId; } @override @@ -57,38 +58,38 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - int getPosition(int textureId) { + int getPosition(int playerId) { log.add('position'); - this.textureId = textureId; + this.playerId = playerId; return 234; } @override - Future seekTo(int position, int textureId) async { + Future seekTo(int position, int playerId) async { log.add('seekTo'); this.position = position; - this.textureId = textureId; + this.playerId = playerId; } @override - void setLooping(bool loop, int textureId) { + void setLooping(bool loop, int playerId) { log.add('setLooping'); looping = loop; - this.textureId = textureId; + this.playerId = playerId; } @override - void setVolume(double volume, int textureId) { + void setVolume(double volume, int playerId) { log.add('setVolume'); this.volume = volume; - this.textureId = textureId; + this.playerId = playerId; } @override - void setPlaybackSpeed(double speed, int textureId) { + void setPlaybackSpeed(double speed, int playerId) { log.add('setPlaybackSpeed'); playbackSpeed = speed; - this.textureId = textureId; + this.playerId = playerId; } } @@ -118,13 +119,17 @@ void main() { }); test('dispose', () async { + player.playerViewStates[1] = + const VideoPlayerTextureViewState(textureId: 1); + await player.dispose(1); expect(log.log.last, 'dispose'); - expect(log.textureId, 1); + expect(log.playerId, 1); + expect(player.playerViewStates, isEmpty); }); test('create with asset', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.asset, asset: 'someAsset', package: 'somePackage', @@ -132,23 +137,13 @@ void main() { expect(log.log.last, 'create'); expect(log.creationOptions?.asset, 'someAsset'); expect(log.creationOptions?.packageName, 'somePackage'); - expect(textureId, 3); - }); - - test('create with incorrect asset throws exception', () async { - try { - await player.create(DataSource( - sourceType: DataSourceType.asset, - asset: '/path/to/incorrect_asset', - )); - fail('should throw PlatformException'); - } catch (e) { - expect(e, isException); - } + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); }); test('create with network', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', formatHint: VideoFormat.dash, @@ -159,11 +154,13 @@ void main() { expect(log.creationOptions?.packageName, null); expect(log.creationOptions?.formatHint, 'dash'); expect(log.creationOptions?.httpHeaders, {}); - expect(textureId, 3); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); }); test('create with network (some headers)', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', httpHeaders: {'Authorization': 'Bearer token'}, @@ -175,36 +172,157 @@ void main() { expect(log.creationOptions?.formatHint, null); expect(log.creationOptions?.httpHeaders, {'Authorization': 'Bearer token'}); - expect(textureId, 3); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); }); test('create with file', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.file, uri: 'someUri', )); expect(log.log.last, 'create'); expect(log.creationOptions?.uri, 'someUri'); - expect(textureId, 3); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); + }); + + test('createWithOptions with asset', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.asset, + asset: 'someAsset', + package: 'somePackage', + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.asset, 'someAsset'); + expect(log.creationOptions?.packageName, 'somePackage'); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); + }); + + test('createWithOptions with network', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'someUri', + formatHint: VideoFormat.dash, + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.asset, null); + expect(log.creationOptions?.uri, 'someUri'); + expect(log.creationOptions?.packageName, null); + expect(log.creationOptions?.formatHint, 'dash'); + expect(log.creationOptions?.httpHeaders, {}); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); + }); + + test('createWithOptions with network (some headers)', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'someUri', + httpHeaders: {'Authorization': 'Bearer token'}, + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.asset, null); + expect(log.creationOptions?.uri, 'someUri'); + expect(log.creationOptions?.packageName, null); + expect(log.creationOptions?.formatHint, null); + expect(log.creationOptions?.httpHeaders, + {'Authorization': 'Bearer token'}); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); + }); + + test('createWithOptions with file', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.uri, 'someUri'); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); + }); + + test('createWithOptions with platform view on iOS', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + ), + viewType: VideoViewType.platformView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.viewType, PlatformVideoViewType.platformView); + expect(playerId, 3); + expect(player.playerViewStates[3], const VideoPlayerPlatformViewState()); + }); + + test('createWithOptions with platform view uses texture view on MacOS', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + ), + viewType: VideoViewType.platformView, + ), + ); + expect(log.log.last, 'create'); + expect(log.creationOptions?.viewType, PlatformVideoViewType.textureView); + expect(playerId, 3); + expect(player.playerViewStates[3], + const VideoPlayerTextureViewState(textureId: 3)); }); test('setLooping', () async { await player.setLooping(1, true); expect(log.log.last, 'setLooping'); - expect(log.textureId, 1); + expect(log.playerId, 1); expect(log.looping, true); }); test('play', () async { await player.play(1); expect(log.log.last, 'play'); - expect(log.textureId, 1); + expect(log.playerId, 1); }); test('pause', () async { await player.pause(1); expect(log.log.last, 'pause'); - expect(log.textureId, 1); + expect(log.playerId, 1); }); test('setMixWithOthers', () async { @@ -220,28 +338,28 @@ void main() { test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume'); - expect(log.textureId, 1); + expect(log.playerId, 1); expect(log.volume, 0.7); }); test('setPlaybackSpeed', () async { await player.setPlaybackSpeed(1, 1.5); expect(log.log.last, 'setPlaybackSpeed'); - expect(log.textureId, 1); + expect(log.playerId, 1); expect(log.playbackSpeed, 1.5); }); test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); - expect(log.textureId, 1); + expect(log.playerId, 1); expect(log.position, 12345); }); test('getPosition', () async { final Duration position = await player.getPosition(1); expect(log.log.last, 'position'); - expect(log.textureId, 1); + expect(log.playerId, 1); expect(position, const Duration(milliseconds: 234)); }); diff --git a/packages/video_player/video_player_avfoundation/test/test_api.g.dart b/packages/video_player/video_player_avfoundation/test/test_api.g.dart index 6dbbb53d233..38ed42f1f9e 100644 --- a/packages/video_player/video_player_avfoundation/test/test_api.g.dart +++ b/packages/video_player/video_player_avfoundation/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -20,8 +20,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is CreationOptions) { + } else if (value is PlatformVideoViewType) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is CreationOptions) { + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -32,6 +38,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoViewType.values[value]; + case 130: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 131: return CreationOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -48,21 +59,21 @@ abstract class TestHostVideoPlayerApi { int create(CreationOptions creationOptions); - void dispose(int textureId); + void dispose(int playerId); - void setLooping(bool isLooping, int textureId); + void setLooping(bool isLooping, int playerId); - void setVolume(double volume, int textureId); + void setVolume(double volume, int playerId); - void setPlaybackSpeed(double speed, int textureId); + void setPlaybackSpeed(double speed, int playerId); - void play(int textureId); + void play(int playerId); - int getPosition(int textureId); + int getPosition(int playerId); - Future seekTo(int position, int textureId); + Future seekTo(int position, int playerId); - void pause(int textureId); + void pause(int playerId); void setMixWithOthers(bool mixWithOthers); @@ -149,11 +160,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.dispose was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.dispose was null, expected non-null int.'); try { - api.dispose(arg_textureId!); + api.dispose(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -184,11 +195,11 @@ abstract class TestHostVideoPlayerApi { final bool? arg_isLooping = (args[0] as bool?); assert(arg_isLooping != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setLooping was null, expected non-null bool.'); - final int? arg_textureId = (args[1] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[1] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setLooping was null, expected non-null int.'); try { - api.setLooping(arg_isLooping!, arg_textureId!); + api.setLooping(arg_isLooping!, arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -219,11 +230,11 @@ abstract class TestHostVideoPlayerApi { final double? arg_volume = (args[0] as double?); assert(arg_volume != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setVolume was null, expected non-null double.'); - final int? arg_textureId = (args[1] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[1] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setVolume was null, expected non-null int.'); try { - api.setVolume(arg_volume!, arg_textureId!); + api.setVolume(arg_volume!, arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -254,11 +265,11 @@ abstract class TestHostVideoPlayerApi { final double? arg_speed = (args[0] as double?); assert(arg_speed != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPlaybackSpeed was null, expected non-null double.'); - final int? arg_textureId = (args[1] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[1] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPlaybackSpeed was null, expected non-null int.'); try { - api.setPlaybackSpeed(arg_speed!, arg_textureId!); + api.setPlaybackSpeed(arg_speed!, arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -286,11 +297,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.play was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.play was null, expected non-null int.'); try { - api.play(arg_textureId!); + api.play(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -318,11 +329,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getPosition was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getPosition was null, expected non-null int.'); try { - final int output = api.getPosition(arg_textureId!); + final int output = api.getPosition(arg_playerId!); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); @@ -353,11 +364,11 @@ abstract class TestHostVideoPlayerApi { final int? arg_position = (args[0] as int?); assert(arg_position != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.seekTo was null, expected non-null int.'); - final int? arg_textureId = (args[1] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[1] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.seekTo was null, expected non-null int.'); try { - await api.seekTo(arg_position!, arg_textureId!); + await api.seekTo(arg_position!, arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -385,11 +396,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.pause was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.pause was null, expected non-null int.'); try { - api.pause(arg_textureId!); + api.pause(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e);