Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
42a5ba4
Make sure the AVPlayerItem is ready to play before sending initialize…
LongCatIsLooong Jun 30, 2025
375ec1f
Merge remote-tracking branch 'upstream/main' into video_player_readyT…
LongCatIsLooong Jun 30, 2025
bb89c41
wuh
LongCatIsLooong Jun 30, 2025
1ad1d2e
docs
LongCatIsLooong Jun 30, 2025
63d240b
example app and README.md
LongCatIsLooong Jul 1, 2025
0cfe3f4
format
LongCatIsLooong Jul 1, 2025
60fc5d2
Update CHANGELOG.md
LongCatIsLooong Jul 7, 2025
f3fbff5
Merge branch 'main' into video_player_readyToPlay
LongCatIsLooong Jul 7, 2025
bcc2725
Update pubspec.yaml
LongCatIsLooong Jul 7, 2025
733f5ea
Update CHANGELOG.md
LongCatIsLooong Jul 7, 2025
c86f933
Merge branch 'main' into video_player_readyToPlay
LongCatIsLooong Jul 8, 2025
35c9a08
Update pubspec.yaml
LongCatIsLooong Jul 8, 2025
3b0cd77
Merge branch 'main' into video_player_readyToPlay
LongCatIsLooong Jul 9, 2025
4315c4a
oops
LongCatIsLooong Jul 10, 2025
47ff5d0
Merge remote-tracking branch 'upstream/main' into video_player_readyT…
LongCatIsLooong Jul 17, 2025
e2ce9fd
add a test
LongCatIsLooong Jul 17, 2025
6f232f5
gemini review
LongCatIsLooong Jul 18, 2025
11e8815
Merge remote-tracking branch 'upstream/main' into video_player_readyT…
LongCatIsLooong Aug 11, 2025
f88925e
comments
LongCatIsLooong Aug 11, 2025
72b7474
formatter
LongCatIsLooong Aug 11, 2025
2179d4e
buh
LongCatIsLooong Aug 11, 2025
5392116
fix test
LongCatIsLooong Aug 11, 2025
7769699
fix test
LongCatIsLooong Aug 11, 2025
5457167
add an assert
LongCatIsLooong Aug 11, 2025
b6ad65a
huh
LongCatIsLooong Aug 11, 2025
349f248
Merge remote-tracking branch 'upstream/main' into video_player_readyT…
LongCatIsLooong Sep 25, 2025
48ecaec
review
LongCatIsLooong Sep 25, 2025
ddf7236
buh
LongCatIsLooong Sep 26, 2025
a74417f
Revert "buh"
LongCatIsLooong Sep 26, 2025
0d58f8c
reformat
LongCatIsLooong Sep 26, 2025
42cc71f
version
LongCatIsLooong Sep 26, 2025
04d3a83
fix test
LongCatIsLooong Sep 29, 2025
ebe1856
fix test
LongCatIsLooong Sep 29, 2025
f3eeb7b
buh
LongCatIsLooong Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## NEXT
## 2.10.1

* Fixes a bug where the `VideoPlayer` widget and `VideoProgressIndicator` widget would stop updating after GlobalKey reparenting.
* Updates the `VideoProgressIndicator` widget to handle zero-duration videos.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 2.10.0
Expand Down
84 changes: 37 additions & 47 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';
import 'dart:io';
import 'dart:math' as math show max;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -79,7 +80,7 @@ class VideoPlayerValue {

/// The total duration of the video.
///
/// The duration is [Duration.zero] if the video hasn't been initialized.
/// The value is only meaningful when [isInitialized] is true.
final Duration duration;

/// The current playback position.
Expand Down Expand Up @@ -430,7 +431,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
_lifeCycleObserver?.initialize();
_creatingCompleter = Completer<void>();

late DataSource dataSourceDescription;
final DataSource dataSourceDescription;
switch (dataSourceType) {
case DataSourceType.asset:
dataSourceDescription = DataSource(
Expand Down Expand Up @@ -863,42 +864,37 @@ class VideoPlayer extends StatefulWidget {
}

class _VideoPlayerState extends State<VideoPlayer> {
_VideoPlayerState() {
_listener = () {
final int newPlayerId = widget.controller.playerId;
if (newPlayerId != _playerId) {
setState(() {
_playerId = newPlayerId;
});
}
};
}

late VoidCallback _listener;

late int _playerId;
void _controllerDidUpdateValue() {
final int newPlayerId = widget.controller.playerId;
if (newPlayerId != _playerId) {
setState(() {
_playerId = newPlayerId;
});
}
}

@override
void initState() {
super.initState();
_playerId = widget.controller.playerId;
// Need to listen for initialization events since the actual widget ID
// becomes available after asynchronous initialization finishes.
widget.controller.addListener(_listener);
widget.controller.addListener(_controllerDidUpdateValue);
}

@override
void didUpdateWidget(VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.controller.removeListener(_listener);
oldWidget.controller.removeListener(_controllerDidUpdateValue);
_playerId = widget.controller.playerId;
widget.controller.addListener(_listener);
widget.controller.addListener(_controllerDidUpdateValue);
}

@override
void deactivate() {
super.deactivate();
widget.controller.removeListener(_listener);
void dispose() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

drive-by fix: deactivate is called either when the state is going to be reparented or disposed.

widget.controller.removeListener(_controllerDidUpdateValue);
super.dispose();
}

@override
Expand Down Expand Up @@ -1089,58 +1085,52 @@ class VideoProgressIndicator extends StatefulWidget {
}

class _VideoProgressIndicatorState extends State<VideoProgressIndicator> {
_VideoProgressIndicatorState() {
listener = () {
if (!mounted) {
return;
}
setState(() {});
};
}

late VoidCallback listener;

VideoPlayerController get controller => widget.controller;

VideoProgressColors get colors => widget.colors;

void _didUpdateControllerValue() {
setState(() {
// The build method reads from controller.value.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The mounted check wasn't necessary. The state will stop listening when the element is unmounted.

});
}

@override
void initState() {
super.initState();
controller.addListener(listener);
controller.addListener(_didUpdateControllerValue);
}

@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
void dispose() {
controller.removeListener(_didUpdateControllerValue);
super.dispose();
}

@override
Widget build(BuildContext context) {
Widget progressIndicator;
final Widget progressIndicator;
if (controller.value.isInitialized) {
final int duration = controller.value.duration.inMilliseconds;
final int position = controller.value.position.inMilliseconds;

int maxBuffering = 0;
for (final DurationRange range in controller.value.buffered) {
final int end = range.end.inMilliseconds;
if (end > maxBuffering) {
maxBuffering = end;
}
}

final double maxBuffering =
duration == 0.0
? 0.0
: controller.value.buffered
.map((DurationRange range) => range.end.inMilliseconds)
.fold(0, math.max) /
duration;
progressIndicator = Stack(
fit: StackFit.passthrough,
children: <Widget>[
LinearProgressIndicator(
value: maxBuffering / duration,
value: maxBuffering,
valueColor: AlwaysStoppedAnimation<Color>(colors.bufferedColor),
backgroundColor: colors.backgroundColor,
),
LinearProgressIndicator(
value: position / duration,
value: duration == 0.0 ? 0.0 : position / duration,
valueColor: AlwaysStoppedAnimation<Color>(colors.playedColor),
backgroundColor: Colors.transparent,
),
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
widgets on Android, iOS, macOS and web.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.10.0
version: 2.10.1

environment:
sdk: ^3.7.0
Expand Down
81 changes: 81 additions & 0 deletions packages/video_player/video_player/test/video_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,87 @@ void main() {
);
});

testWidgets(
'VideoPlayer still listens for controller changes when reparented',
(WidgetTester tester) async {
final FakeController controller = FakeController();
addTearDown(controller.dispose);
final GlobalKey videoKey = GlobalKey();
final Widget videoPlayer = KeyedSubtree(
key: videoKey,
child: VideoPlayer(controller),
);

await tester.pumpWidget(videoPlayer);
expect(find.byType(Texture), findsNothing);

// The VideoPlayer is reparented in the widget tree, before the
// underlying player is initialized.
await tester.pumpWidget(SizedBox(child: videoPlayer));
controller.playerId = 321;
controller.value = controller.value.copyWith(
duration: const Duration(milliseconds: 100),
isInitialized: true,
);

await tester.pump();
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Texture && widget.textureId == 321,
),
findsOneWidget,
);
},
);

testWidgets(
'VideoProgressIndicator still listens for controller changes after reparenting',
(WidgetTester tester) async {
final FakeController controller = FakeController();
addTearDown(controller.dispose);
final GlobalKey key = GlobalKey();
final Widget progressIndicator = VideoProgressIndicator(
key: key,
controller,
allowScrubbing: false,
);

controller.value = controller.value.copyWith(
duration: const Duration(milliseconds: 100),
position: const Duration(milliseconds: 50),
isInitialized: true,
);
await tester.pumpWidget(MaterialApp(home: progressIndicator));
await tester.pump();
await tester.pumpWidget(
MaterialApp(home: SizedBox(child: progressIndicator)),
);
expect((key.currentContext! as Element).dirty, isFalse);
// Verify that changing value dirties the widget tree.
controller.value = controller.value.copyWith(
position: const Duration(milliseconds: 100),
);
expect((key.currentContext! as Element).dirty, isTrue);
},
);

testWidgets('VideoPlayer does not crash after loading 0-duration videos', (
WidgetTester tester,
) async {
final FakeController controller = FakeController();
addTearDown(controller.dispose);
controller.value = controller.value.copyWith(
duration: Duration.zero,
isInitialized: true,
);
await tester.pumpWidget(
MaterialApp(
home: VideoProgressIndicator(controller, allowScrubbing: false),
),
);
expect(tester.takeException(), isNull);
});

testWidgets('non-zero rotationCorrection value is used', (
WidgetTester tester,
) async {
Expand Down
5 changes: 5 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.8.14
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should revert the CHANGELOG and version change in video_player_android; the changes here are only to an unpublished file.


* Fixes an issue in the example app that some widgets stop updating after GlobalKey reparenting.
* Updates the `VideoProgressIndicator` widget in the example app to handle zero-duration videos.

## 2.8.13

* Bumps com.android.tools.build:gradle to 8.12.1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import 'dart:async';
import 'dart:io';
import 'dart:math' as math show max;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -433,9 +434,9 @@ class _VideoPlayerState extends State<VideoPlayer> {
}

@override
void deactivate() {
super.deactivate();
void dispose() {
widget.controller.removeListener(_listener);
super.dispose();
}

@override
Expand Down Expand Up @@ -535,9 +536,9 @@ class _VideoProgressIndicatorState extends State<VideoProgressIndicator> {
}

@override
void deactivate() {
void dispose() {
controller.removeListener(listener);
super.deactivate();
super.dispose();
}

@override
Expand All @@ -546,29 +547,29 @@ class _VideoProgressIndicatorState extends State<VideoProgressIndicator> {
const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2);
const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5);

Widget progressIndicator;
final Widget progressIndicator;
if (controller.value.isInitialized) {
final int duration = controller.value.duration.inMilliseconds;
final int position = controller.value.position.inMilliseconds;

int maxBuffering = 0;
for (final DurationRange range in controller.value.buffered) {
final int end = range.end.inMilliseconds;
if (end > maxBuffering) {
maxBuffering = end;
}
}
final double maxBuffering =
duration == 0.0
? 0.0
: controller.value.buffered
.map((DurationRange range) => range.end.inMilliseconds)
.fold(0, math.max) /
duration;

progressIndicator = Stack(
fit: StackFit.passthrough,
children: <Widget>[
LinearProgressIndicator(
value: maxBuffering / duration,
value: maxBuffering,
valueColor: const AlwaysStoppedAnimation<Color>(bufferedColor),
backgroundColor: backgroundColor,
),
LinearProgressIndicator(
value: position / duration,
value: duration == 0.0 ? 0.0 : position / duration,
valueColor: const AlwaysStoppedAnimation<Color>(playedColor),
backgroundColor: Colors.transparent,
),
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_android
description: Android implementation of the video_player plugin.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.8.13
version: 2.8.14

environment:
sdk: ^3.7.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## NEXT
## 2.8.5

* Fixes a bug where the video player fails to initialize when `AVFoundation` reports a duration of zero.
* Fixes a bug in the example app that some widgets stop updating after GlobalKey reparenting.
* Updates the `VideoProgressIndicator` widget in the example app to handle zero-duration videos.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 2.8.4
Expand Down
Loading