Skip to content

Conversation

@ToddZeil
Copy link
Contributor

@ToddZeil ToddZeil commented Jan 18, 2024

When a video is complete, and not on a loop setting.
Pause() function is called even if video is already paused.

(_applyPlayPause() function is used for play and pause which can lead to playing a video when attempting to restart)

Fixes flutter/flutter#143141

Edited: Also this may only be an issue on web

Pre-launch Checklist

  • [ y] I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • [ y] I read the Tree Hygiene wiki page, which explains my responsibilities.
  • [ y] I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use dart format.)
  • [y ] I signed the CLA.
  • [y ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • [ y] I listed at least one issue that this PR fixes in the description above.
  • [y ] I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • [y ] I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • [not needed ] I updated/added relevant documentation (doc comments with ///).
  • [fixed curren, added one more test ] I added new tests to check the change I am making, or this PR is test-exempt.
  • [Y, one fixed test ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@ToddZeil ToddZeil requested a review from tarrinneal as a code owner January 18, 2024 03:44
@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact "@test-exemption-reviewer" in the #hackers channel in Chat (don't just cc them here, they won't see it! Use Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Copy link
Contributor

@tarrinneal tarrinneal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple nits here.

I'm not certain if this will break the work-around for the video completing but not truly reaching the last frame or not. Maybe @stuartmorgan has some insight.

Comment on lines 1 to 4
## NEXT

* Description of the change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed

* Updates support matrix in README to indicate that iOS 11 is no longer supported.
* Clients on versions of Flutter that still support iOS 11 can continue to use this
package with iOS 11, but will not receive any further updates to the iOS implementation.
* Fix infinite pause loop caused by on completed to call pause even when video is pause.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes infinite pause loop caused by call to pause even when video is already paused.

@ToddZeil
Copy link
Contributor Author

A couple nits here.

I'm not certain if this will break the work-around for the video completing but not truly reaching the last frame or not. Maybe @stuartmorgan has some insight.

There are a couple of other ways we could do this if your not sure and think they will be better.
Either, split up the play pause into two different functions, or only call the pause part of the function if the timer is not null

pause().then((void pauseResult) => seekTo(value.duration));
if (value.isPlaying) {
pause().then((void pauseResult) => seekTo(value.duration));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changed logic would definitely not be guaranteed to actually do what this code is supposed to be doing; if the native side reported reaching the end as:

  • playback stopped
  • video completed

then the seek code would never run.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stuartmorgan have updated the logic to account for this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I believe this infinite loop issue is only seen on web for some reason

Copy link
Contributor

@tarrinneal tarrinneal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change tracks for me, approved pending @stuartmorgan review.

Copy link
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code more deeply here, it's not clear to me what the reason for this change is. Why is calling pause while paused a problem? The PR description just says that it's a problem because it calls _applyPlayPause(), but with the paused value state, that will call _videoPlayerPlatform.pause. If calling pause on the platform object will start playing a video, that seems like a platform bug.

Could you file an issue with clear repro steps and an explanation of what exactly is happening?

});

test(
'isCompleted seeks position to max duration and pauses the video first if is playing',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the thing you want to fix that you want to not call pause if it's not playing? That path would also need a test.

@stuartmorgan-g
Copy link
Collaborator

Thanks for filing the issue. Could you elaborate, either here or (preferably) in the issue about what exactly is going wrong? It's not clear to me how the change in this PR relates to the problem shown in the issue. What exactly is happening in the web implementation that is causing the incorrect behavior?

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 8, 2024

Thanks for filing the issue. Could you elaborate, either here or (preferably) in the issue about what exactly is going wrong? It's not clear to me how the change in this PR relates to the problem shown in the issue. What exactly is happening in the web implementation that is causing the incorrect behavior?

The listener when completed goes into an infinite loop which continuously calls any functions in the onCompleted switch case statement.
Specifically this is repeatedly called
case VideoEventType.completed: // In this case we need to stop _timer, set isPlaying=false, and // position=value.duration. Instead of setting the values directly, // we use pause() and seekTo() to ensure the platform stops playing // and seeks to the last frame of the video. pause().then((void pauseResult) => seekTo(value.duration)); value = value.copyWith(isCompleted: true);

@stuartmorgan-g
Copy link
Collaborator

The listener when completed goes into an infinite loop which continuously calls any functions in the onCompleted switch case statement.

It's not clear to me why this would be the case. Could you provide the specific call sequence in the infinite loop? In other words, when you say:

Specifically this is repeatedly called

what code is repeatedly calling that?

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 8, 2024

The listener when completed goes into an infinite loop which continuously calls any functions in the onCompleted switch case statement.

It's not clear to me why this would be the case. Could you provide the specific call sequence in the infinite loop? In other words, when you say:

Specifically this is repeatedly called

what code is repeatedly calling that?

This is the code which seems to be repeatedly calling it

/.pub-cache/hosted/pub.dev/video_player_web-2.1.3/lib/src/video_player.dart

_videoElement.onEnded.listen((dynamic _) { setBuffering(false); _eventController.add(VideoEvent(eventType: VideoEventType.completed)); });

@stuartmorgan-g
Copy link
Collaborator

It seems like more root-causing is still needed here. As far as I can see the only change the current version of the PR makes is not calling pause() when paused, but pause() on web just calls pause() on the <video> element, and according to MDN:

The HTMLMediaElement.pause() method will pause playback of the media, if the media is already in a paused state this method will have no effect.

Are those docs incorrect? Is calling pause() when already paused at the end of the video triggering an onEnded event? That seems very surprising if so, but if not I don't see how this PR would fix the behavior.

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 8, 2024 via email

@stuartmorgan-g
Copy link
Collaborator

So this PR doesn't fix the infinite loop, just removes one side effect of the infinite loop?

If there's an infinite loop, we should root cause and fix the loop itself, not the side effect.

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 8, 2024 via email

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 11, 2024

The loop is coming from
'flutter/flutter/bin/cache/pkg/sky_engine/lib/html/html_dart2js.dart'
Specifically: ElementStream<Event> get onEnded => endedEvent.forElement(this);

This is used in video_player_web

_videoElement.onEnded.listen((dynamic _) { setBuffering(false); _eventController.add(VideoEvent(eventType: VideoEventType.completed)); });

I can't find the code where the event is added to the stream, I'm not sure this is even in our control ?

@stuartmorgan-g
Copy link
Collaborator

The creation of the event is done by the browser, but it is likely the result of something the plugin is doing; it's extremely unlikely that browser behavior is to create an infinite stream of onEnded events without any input.

Is seeking to the end when already at the end creating a new onEnded?

@ToddZeil
Copy link
Contributor Author

Thanks, have updated now

* Updates support matrix in README to indicate that iOS 11 is no longer supported.
* Clients on versions of Flutter that still support iOS 11 can continue to use this
package with iOS 11, but will not receive any further updates to the iOS implementation.
* Web, Fix infinite pause loop caused by seekTo marking the video as completed when already completed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, revert all changes to the video_player package, they're not needed.

flutter pub get (or flutter pub upgrade) will take care of fetching the fixed version of video_player_web once the fix is published.

(Sorry I wasn't clearer before!)

@ToddZeil
Copy link
Contributor Author

Thanks for all the help guys, should be all done now!

Copy link
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments; I'll leave final approval for @ditman since this is now web-only.

## NEXT
## 2.1.4

* Fixes infinite pause loop caused by seekTo marking the video as completed when already completed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please use backticks on seekTo to mark it as code.

void seekTo(Duration position) {
assert(!position.isNegative);
// Don't seek if video is already at position.
// As seeking when completed will trigger another completed event ('onEnded')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a sentence fragment; please combine it with the line above: "Don't seek if the video is already at the target position, as seeking when completed [...]"

assert(!position.isNegative);
// Don't seek if video is already at position.
// As seeking when completed will trigger another completed event ('onEnded')
// to avoid potentially firing extra 'onEnded' events when the video is already over
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"when the video is already over" is redundant with the earlier "when completed", so should be removed.

assert(!position.isNegative);
// Don't seek if video is already at position.
// As seeking when completed will trigger another completed event ('onEnded')
// to avoid potentially firing extra 'onEnded' events when the video is already over
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a period at the end of the sentence, since comments should be properly punctuated sentences.

@stuartmorgan-g stuartmorgan-g self-requested a review February 20, 2024 21:11
Copy link
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Fixing review state; I thought I could clear my request without approving, but apparently not.)

@ditman
Copy link
Member

ditman commented Feb 20, 2024

Adding a small commit here to address @stuartmorgan's concerns, and get the branch updated with the latest main. I take this back, it seems @ToddZeil is fixing stuff! Thanks @ToddZeil!

@ToddZeil
Copy link
Contributor Author

Thanks for the review, updated now.
So you can check again when when you can @ditman

Copy link
Member

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a test, I think we can write one easily by listening to the events coming from the videoplayer (two seeks to the same position should only yield a single seek event to the core plugin)

@ToddZeil
Copy link
Contributor Author

Okay where do we put tests for this package ?
Will it go in here packages/packages/video_player/video_player/test/video_player_test.dart ?

@ditman
Copy link
Member

ditman commented Feb 20, 2024

@ToddZeil there's already several tests here:

TL;DR: Assuming that chromedriver and dart are in your $PATH, and Chrome is in a standard location, inside the video_player_web directory, you can do something like this:

PLUGIN_TOOLS=/work/flutter/packages/script/tool/bin/flutter_plugin_tools.dart # Adjust this
dart $PLUGIN_TOOLS drive-examples --current-package --web --run-chromedriver

(I normally use Linux)

There's more instructions on how to run tests in the README.md of the example

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 21, 2024

I have set up the integration tests, not sure exactly how these work but the current seekTo doesn't seem to check for anything other than completion ?

testWidgets('can seek to position', (WidgetTester tester) async { expect( VideoPlayerPlatform.instance.seekTo( await textureId, const Duration(seconds: 1), ), completes, ); });

Adding seekTo seems to add the following events to the tests:
` VideoEventType:VideoEventType.bufferingStart,
        VideoEventType:VideoEventType.bufferingUpdate,
        VideoEventType:VideoEventType.isPlayingStateUpdate,
        VideoEventType:VideoEventType.bufferingEnd,
        VideoEventType:VideoEventType.completed`

But when I comment out the new code it doesn't seem to effect anything with in the test?
It doesn't seem to call completed multiple times?
Could I be doing something wrong?

@ditman
Copy link
Member

ditman commented Feb 22, 2024

Could I be doing something wrong?

@ToddZeil can you share the code of the test that you've written? Or I can try and write one tomorrow :)

My plan is to:

  • start a video paused
  • seek to a position
  • assert the events (probably seeked or some combination of buffering?)
  • seek to the same position
  • assert that no new events fired in a reasonable amount of time (500ms or similar).

I that test should fail if we comment out the fix (?)

@ToddZeil
Copy link
Contributor Author

ToddZeil commented Feb 22, 2024

Your test sounds like it should work!
The main issue was happening when looping was off and seekTo was seeking to complete the video

I was just trying to find out what events the seekTo function emitted, but I couldn't get multiple events firing through

This is the test I was trying:

testWidgets('multiple calls to seekTo will emit a seek event',
        (WidgetTester tester) async {
      final int videoPlayerId = await textureId;
      final Stream<VideoEvent> eventStream =
          VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId);

      final Future<List<VideoEvent>> stream = eventStream.timeout(
        const Duration(seconds: 2),
        onTimeout: (EventSink<VideoEvent> sink) {
          sink.close();
        },
      ).toList();

      await VideoPlayerPlatform.instance.setLooping(videoPlayerId, false);
      await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0);

      await VideoPlayerPlatform.instance.play(videoPlayerId);


      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(milliseconds: 300));

      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(milliseconds: 350));

      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(milliseconds: 350));

      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(seconds: 99));

      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(seconds: 99));

      await VideoPlayerPlatform.instance
          .seekTo(await textureId, const Duration(milliseconds: 100));

      // Let the video play, until we stop seeing events for two seconds
      final List<VideoEvent> events = await stream;

      await VideoPlayerPlatform.instance.pause(videoPlayerId);

      // The expected list of event types should look like this:
      // 1. isPlayingStateUpdate (videoElement.onPlaying)
      // 2. bufferingStart,
      // 3. bufferingUpdate (videoElement.onWaiting),
      // 4. initialized (videoElement.onCanPlay),
      // 5. bufferingEnd (videoElement.onCanPlayThrough),
      expect(
          events.map((VideoEvent e) => e.eventType),
          equals(<VideoEventType>[
            VideoEventType.isPlayingStateUpdate,
            VideoEventType.bufferingStart,
            VideoEventType.bufferingUpdate,
            VideoEventType.initialized,
            VideoEventType.bufferingEnd,
          ]));
    });

@ToddZeil
Copy link
Contributor Author

Did get a chance to look into this test ? Or have further hints or advice ? Thanks

@ditman
Copy link
Member

ditman commented Feb 29, 2024

@ToddZeil I didn't have time to look at the test, but I'm going to work next in migrating the video_player_web to the latest package:web, so I can sneak your fix there :)

The problem is that we don't have any "seek" event that we can check that wasn't propagated:

(We could maybe inject a fake video player and ensure we don't call "seek" multiple times, but I'm not sure... I'll try to add you as a Co-Author in the migration PR!)

@ToddZeil
Copy link
Contributor Author

Okay interesting!

Cool hopefully we can get it in, thanks for that :)

ditman added a commit to balvinderz/packages that referenced this pull request Mar 5, 2024
@ditman
Copy link
Member

ditman commented Mar 6, 2024

This fix has landed alongside #5800! Closing.

@ditman ditman closed this Mar 6, 2024
@ToddZeil
Copy link
Contributor Author

ToddZeil commented Mar 6, 2024

Great thank you!

@ditman
Copy link
Member

ditman commented Mar 6, 2024

@ToddZeil thanks for taking the time to report and fix this, keep it coming!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[video_player] [WEB] Video breaks when trying to restart finished video on chrome

4 participants