diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index 557fe15614bb..5c6f92135c6e 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -17025dd88227cd9532c33fa78f5250d548d87e9a +68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 76b14ba3f961..9c6f47ee7dd4 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.7 + +* Fixes playback speed resetting. + ## 2.6.6 * Fixes changing global audio session category to be collision free across plugins. 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 559c9f089d6d..4150250d168c 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -672,6 +672,8 @@ - (void)testSeekToleranceWhenSeekingToEnd { // Change playback speed. [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error]; XCTAssertNil(error); + [videoPlayerPlugin playPlayer:textureId.integerValue error:&error]; + XCTAssertNil(error); XCTAssertEqual(avPlayer.rate, 2); XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate); @@ -839,6 +841,41 @@ - (void)testFailedToLoadVideoEventShouldBeAlwaysSent { [self waitForExpectationsWithTimeout:10.0 handler:nil]; } +- (void)testUpdatePlayingStateShouldNotResetRate { + NSObject *registrar = + [GetPluginRegistry() registrarForPlugin:@"testUpdatePlayingStateShouldNotResetRate"]; + + FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] + initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:nil] + displayLinkFactory:nil + registrar:registrar]; + + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + FVPCreationOptions *create = [FVPCreationOptions + makeWithAsset:nil + 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]; + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + [player onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"initialized"]) { + [initializedExpectation fulfill]; + } + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error]; + [videoPlayerPlugin playPlayer:textureId.integerValue error:&error]; + XCTAssertEqual(player.player.rate, 2); +} + #if TARGET_OS_IOS - (void)testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker { NSObject *registrar = [GetPluginRegistry() 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 5892274a37db..087cf401db54 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 @@ -29,6 +29,8 @@ @interface FVPVideoPlayer () @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. @@ -323,7 +325,15 @@ - (void)updatePlayingState { return; } if (_isPlaying) { - [_player play]; + // Calling play is the same as setting the rate to 1.0 (or to defaultRate depending on iOS + // version) so last set playback speed must be set here if any instead. + // https://github.com/flutter/flutter/issues/71264 + // https://github.com/flutter/flutter/issues/73643 + if (_targetPlaybackSpeed) { + [self updateRate]; + } else { + [_player play]; + } } else { [_player pause]; } @@ -332,6 +342,32 @@ - (void)updatePlayingState { _displayLink.running = _isPlaying || self.waitingForFrame; } +/// Synchronizes the player's playback rate with targetPlaybackSpeed, constrained by the playback +/// rate capabilities of the player's current item. +- (void)updateRate { + // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of + // these checks. + // If status is not AVPlayerItemStatusReadyToPlay then both canPlayFastForward + // and canPlaySlowForward are always false and it is unknown whether video can + // be played at these speeds, updatePlayingState will be called again when + // status changes to AVPlayerItemStatusReadyToPlay. + float speed = _targetPlaybackSpeed.floatValue; + BOOL readyToPlay = _player.currentItem.status == AVPlayerItemStatusReadyToPlay; + if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { + if (!readyToPlay) { + return; + } + speed = 2.0; + } + if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { + if (!readyToPlay) { + return; + } + speed = 1.0; + } + _player.rate = speed; +} + - (void)sendFailedToLoadVideoEvent { if (_eventSink == nil) { return; @@ -473,27 +509,8 @@ - (void)setVolume:(double)volume { } - (void)setPlaybackSpeed:(double)speed { - // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of - // these checks. - if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { - if (_eventSink != nil) { - _eventSink([FlutterError errorWithCode:@"VideoError" - message:@"Video cannot be fast-forwarded beyond 2.0x" - details:nil]); - } - return; - } - - if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { - if (_eventSink != nil) { - _eventSink([FlutterError errorWithCode:@"VideoError" - message:@"Video cannot be slow-forwarded" - details:nil]); - } - return; - } - - _player.rate = speed; + _targetPlaybackSpeed = @(speed); + [self updatePlayingState]; } - (CVPixelBufferRef)copyPixelBuffer { diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index c20357d78044..c7f9dadb777b 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.6 +version: 2.6.7 environment: sdk: ^3.4.0