Skip to content

Commit 9ea2bef

Browse files
authored
[video_player_avfoundation] fix playback speed resetting (#7657)
Calling play is the same as setting the rate to 1.0 (or to `defaultRate` depending on ios version, *documentation in this first link is clearly not updated because it does not mention `defaultRate`*): https://developer.apple.com/documentation/avfoundation/avplayer/1386726-play?language=objc https://developer.apple.com/documentation/avfoundation/avplayer/3929373-defaultrate?language=objc *The second link contains a note about not starting playback by setting the rate to 1.0. I assume this is because of the introduction of `defaultRate` (which can be different than 1.0) and not because `play` may do something more than just setting `rate` as that wording is explicit about setting rate to 1.0, it says nothing about any other value.* This is also why flutter/plugins#4331 did not work well. It was setting `rate` to 1.0 (through `play`) then immediately to the value set by `setPlaybackSpeed`. One of them or both caused change to `playbackLikelyToKeepUp` which triggered `observeValueForKeyPath` with `playbackLikelyToKeepUpContext` which in turn called again `updatePlayingState` where was `rate` again changed first to 1.0 then to another value and so on. In this PR the rate is changed only once and then to the same value, seems when `rate` is assigned but not really changed it does not trigger anything. In issues below `seekTo` can trigger `playbackLikelyToKeepUp` change which will call `updatePlayingState` and reset `rate` to 1.0 through `play`. When the speed is set in dart before initialization then the dart side will set it right after calling `play` but even some time after initialization there is still a flood of events calling `updatePlayingState` and it is likely that some of them will call it after `setPlaybackSpeed`. Actually even when `setPlaybackSpeed` was not called on dart side it (dart side) will always change speed after play to 1.0 so it ignores this new `defaultRate` feature. - fixes flutter/flutter#71264 - fixes flutter/flutter#73643
1 parent f73cb00 commit 9ea2bef

File tree

4 files changed

+81
-23
lines changed

4 files changed

+81
-23
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.6.7
2+
3+
* Fixes playback speed resetting.
4+
15
## 2.6.6
26

37
* Fixes changing global audio session category to be collision free across plugins.

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ - (void)testSeekToleranceWhenSeekingToEnd {
672672
// Change playback speed.
673673
[videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error];
674674
XCTAssertNil(error);
675+
[videoPlayerPlugin playPlayer:textureId.integerValue error:&error];
676+
XCTAssertNil(error);
675677
XCTAssertEqual(avPlayer.rate, 2);
676678
XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate);
677679

@@ -839,6 +841,41 @@ - (void)testFailedToLoadVideoEventShouldBeAlwaysSent {
839841
[self waitForExpectationsWithTimeout:10.0 handler:nil];
840842
}
841843

844+
- (void)testUpdatePlayingStateShouldNotResetRate {
845+
NSObject<FlutterPluginRegistrar> *registrar =
846+
[GetPluginRegistry() registrarForPlugin:@"testUpdatePlayingStateShouldNotResetRate"];
847+
848+
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
849+
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:nil]
850+
displayLinkFactory:nil
851+
registrar:registrar];
852+
853+
FlutterError *error;
854+
[videoPlayerPlugin initialize:&error];
855+
XCTAssertNil(error);
856+
FVPCreationOptions *create = [FVPCreationOptions
857+
makeWithAsset:nil
858+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
859+
packageName:nil
860+
formatHint:nil
861+
httpHeaders:@{}];
862+
NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error];
863+
FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId];
864+
865+
XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
866+
[player onListenWithArguments:nil
867+
eventSink:^(NSDictionary<NSString *, id> *event) {
868+
if ([event[@"event"] isEqualToString:@"initialized"]) {
869+
[initializedExpectation fulfill];
870+
}
871+
}];
872+
[self waitForExpectationsWithTimeout:10 handler:nil];
873+
874+
[videoPlayerPlugin setPlaybackSpeed:2 forPlayer:textureId.integerValue error:&error];
875+
[videoPlayerPlugin playPlayer:textureId.integerValue error:&error];
876+
XCTAssertEqual(player.player.rate, 2);
877+
}
878+
842879
#if TARGET_OS_IOS
843880
- (void)testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker {
844881
NSObject<FlutterPluginRegistrar> *registrar = [GetPluginRegistry()

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ @interface FVPVideoPlayer ()
2929
@property(nonatomic) CGAffineTransform preferredTransform;
3030
/// Indicates whether the video player is currently playing.
3131
@property(nonatomic, readonly) BOOL isPlaying;
32+
/// The target playback speed requested by the plugin client.
33+
@property(nonatomic, readonly) NSNumber *targetPlaybackSpeed;
3234
/// Indicates whether the video player has been initialized.
3335
@property(nonatomic, readonly) BOOL isInitialized;
3436
/// The updater that drives callbacks to the engine to indicate that a new frame is ready.
@@ -323,7 +325,15 @@ - (void)updatePlayingState {
323325
return;
324326
}
325327
if (_isPlaying) {
326-
[_player play];
328+
// Calling play is the same as setting the rate to 1.0 (or to defaultRate depending on iOS
329+
// version) so last set playback speed must be set here if any instead.
330+
// https://github.com/flutter/flutter/issues/71264
331+
// https://github.com/flutter/flutter/issues/73643
332+
if (_targetPlaybackSpeed) {
333+
[self updateRate];
334+
} else {
335+
[_player play];
336+
}
327337
} else {
328338
[_player pause];
329339
}
@@ -332,6 +342,32 @@ - (void)updatePlayingState {
332342
_displayLink.running = _isPlaying || self.waitingForFrame;
333343
}
334344

345+
/// Synchronizes the player's playback rate with targetPlaybackSpeed, constrained by the playback
346+
/// rate capabilities of the player's current item.
347+
- (void)updateRate {
348+
// See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of
349+
// these checks.
350+
// If status is not AVPlayerItemStatusReadyToPlay then both canPlayFastForward
351+
// and canPlaySlowForward are always false and it is unknown whether video can
352+
// be played at these speeds, updatePlayingState will be called again when
353+
// status changes to AVPlayerItemStatusReadyToPlay.
354+
float speed = _targetPlaybackSpeed.floatValue;
355+
BOOL readyToPlay = _player.currentItem.status == AVPlayerItemStatusReadyToPlay;
356+
if (speed > 2.0 && !_player.currentItem.canPlayFastForward) {
357+
if (!readyToPlay) {
358+
return;
359+
}
360+
speed = 2.0;
361+
}
362+
if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) {
363+
if (!readyToPlay) {
364+
return;
365+
}
366+
speed = 1.0;
367+
}
368+
_player.rate = speed;
369+
}
370+
335371
- (void)sendFailedToLoadVideoEvent {
336372
if (_eventSink == nil) {
337373
return;
@@ -473,27 +509,8 @@ - (void)setVolume:(double)volume {
473509
}
474510

475511
- (void)setPlaybackSpeed:(double)speed {
476-
// See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of
477-
// these checks.
478-
if (speed > 2.0 && !_player.currentItem.canPlayFastForward) {
479-
if (_eventSink != nil) {
480-
_eventSink([FlutterError errorWithCode:@"VideoError"
481-
message:@"Video cannot be fast-forwarded beyond 2.0x"
482-
details:nil]);
483-
}
484-
return;
485-
}
486-
487-
if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) {
488-
if (_eventSink != nil) {
489-
_eventSink([FlutterError errorWithCode:@"VideoError"
490-
message:@"Video cannot be slow-forwarded"
491-
details:nil]);
492-
}
493-
return;
494-
}
495-
496-
_player.rate = speed;
512+
_targetPlaybackSpeed = @(speed);
513+
[self updatePlayingState];
497514
}
498515

499516
- (CVPixelBufferRef)copyPixelBuffer {

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_avfoundation
22
description: iOS and macOS implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.6.6
5+
version: 2.6.7
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)