Skip to content

Commit 8178bf1

Browse files
nichtverstehensigurdm
authored andcommitted
Update VideoPlayer widget when the attached controller changes (flutter#769)
* Update textureId in VideoPlayer widget when the attached controller changes. This fixes a bug introduced in flutter#767: if a State is attached to a new widget it never switched to the new controller. Also this change makes sure to remove listeners during deinitialization. * Add tests for conditions identified in flutter/flutter#21483. This is a breaking change since it reduces the public interface of VideoPlayerController to make it easier to fake it in the test (and hide unnecessary implementation details).
1 parent 21bc07c commit 8178bf1

File tree

4 files changed

+144
-31
lines changed

4 files changed

+144
-31
lines changed

packages/video_player/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.7.0
2+
3+
* Add a unit test for controller and texture changes. This is a breaking change since the interface
4+
had to be cleaned up to facilitate faking.
5+
6+
## 0.6.6
7+
8+
* Fix the condition where the player doesn't update when attached controller is changed.
9+
110
## 0.6.5
211

312
* Eliminate race conditions around initialization: now initialization events are queued and guaranteed

packages/video_player/lib/video_player.dart

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
148148
/// is constructed with.
149149
final DataSourceType dataSourceType;
150150

151-
String package;
152-
Timer timer;
153-
bool isDisposed = false;
151+
final String package;
152+
Timer _timer;
153+
bool _isDisposed = false;
154154
Completer<void> _creatingCompleter;
155155
StreamSubscription<dynamic> _eventSubscription;
156156
_VideoAppLifeCycleObserver _lifeCycleObserver;
@@ -171,6 +171,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
171171
/// null.
172172
VideoPlayerController.network(this.dataSource)
173173
: dataSourceType = DataSourceType.network,
174+
package = null,
174175
super(new VideoPlayerValue(duration: null));
175176

176177
/// Constructs a [VideoPlayerController] playing a video from a file.
@@ -180,8 +181,12 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
180181
VideoPlayerController.file(File file)
181182
: dataSource = 'file://${file.path}',
182183
dataSourceType = DataSourceType.file,
184+
package = null,
183185
super(new VideoPlayerValue(duration: null));
184186

187+
// Visible for testing.
188+
int get textureId => _textureId;
189+
185190
Future<void> initialize() async {
186191
_lifeCycleObserver = new _VideoAppLifeCycleObserver(this);
187192
_lifeCycleObserver.initialize();
@@ -231,7 +236,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
231236
break;
232237
case 'completed':
233238
value = value.copyWith(isPlaying: false);
234-
timer?.cancel();
239+
_timer?.cancel();
235240
break;
236241
case 'bufferingUpdate':
237242
final List<dynamic> values = map['values'];
@@ -251,7 +256,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
251256
void errorListener(Object obj) {
252257
final PlatformException e = obj;
253258
value = new VideoPlayerValue.erroneous(e.message);
254-
timer?.cancel();
259+
_timer?.cancel();
255260
}
256261

257262
_eventSubscription = _eventChannelFor(_textureId)
@@ -268,9 +273,9 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
268273
Future<void> dispose() async {
269274
if (_creatingCompleter != null) {
270275
await _creatingCompleter.future;
271-
if (!isDisposed) {
272-
isDisposed = true;
273-
timer?.cancel();
276+
if (!_isDisposed) {
277+
_isDisposed = true;
278+
_timer?.cancel();
274279
await _eventSubscription?.cancel();
275280
await _channel.invokeMethod(
276281
'dispose',
@@ -279,7 +284,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
279284
}
280285
_lifeCycleObserver.dispose();
281286
}
282-
isDisposed = true;
287+
_isDisposed = true;
283288
super.dispose();
284289
}
285290

@@ -299,7 +304,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
299304
}
300305

301306
Future<void> _applyLooping() async {
302-
if (!value.initialized || isDisposed) {
307+
if (!value.initialized || _isDisposed) {
303308
return;
304309
}
305310
_channel.invokeMethod(
@@ -309,29 +314,29 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
309314
}
310315

311316
Future<void> _applyPlayPause() async {
312-
if (!value.initialized || isDisposed) {
317+
if (!value.initialized || _isDisposed) {
313318
return;
314319
}
315320
if (value.isPlaying) {
316321
await _channel.invokeMethod(
317322
'play',
318323
<String, dynamic>{'textureId': _textureId},
319324
);
320-
timer = new Timer.periodic(
325+
_timer = new Timer.periodic(
321326
const Duration(milliseconds: 500),
322327
(Timer timer) async {
323-
if (isDisposed) {
328+
if (_isDisposed) {
324329
return;
325330
}
326331
final Duration newPosition = await position;
327-
if (isDisposed) {
332+
if (_isDisposed) {
328333
return;
329334
}
330335
value = value.copyWith(position: newPosition);
331336
},
332337
);
333338
} else {
334-
timer?.cancel();
339+
_timer?.cancel();
335340
await _channel.invokeMethod(
336341
'pause',
337342
<String, dynamic>{'textureId': _textureId},
@@ -340,7 +345,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
340345
}
341346

342347
Future<void> _applyVolume() async {
343-
if (!value.initialized || isDisposed) {
348+
if (!value.initialized || _isDisposed) {
344349
return;
345350
}
346351
await _channel.invokeMethod(
@@ -351,7 +356,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
351356

352357
/// The position in the current video.
353358
Future<Duration> get position async {
354-
if (isDisposed) {
359+
if (_isDisposed) {
355360
return null;
356361
}
357362
return new Duration(
@@ -363,7 +368,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
363368
}
364369

365370
Future<void> seekTo(Duration moment) async {
366-
if (isDisposed) {
371+
if (_isDisposed) {
367372
return;
368373
}
369374
if (moment > value.duration) {
@@ -430,29 +435,48 @@ class VideoPlayer extends StatefulWidget {
430435
}
431436

432437
class _VideoPlayerState extends State<VideoPlayer> {
433-
int textureId;
438+
VoidCallback _listener;
439+
int _textureId;
440+
441+
_VideoPlayerState() {
442+
_listener = () {
443+
final int newTextureId = widget.controller.textureId;
444+
if (newTextureId != _textureId) {
445+
setState(() {
446+
_textureId = newTextureId;
447+
});
448+
}
449+
};
450+
}
434451

435452
@override
436453
void initState() {
437454
super.initState();
438-
textureId = widget.controller._textureId;
455+
_textureId = widget.controller.textureId;
439456
// Need to listen for initialization events since the actual texture ID
440457
// becomes available after asynchronous initialization finishes.
441-
widget.controller.addListener(() {
442-
final int newTextureId = widget.controller._textureId;
443-
if (newTextureId != textureId) {
444-
setState(() {
445-
textureId = newTextureId;
446-
});
447-
}
448-
});
458+
widget.controller.addListener(_listener);
459+
}
460+
461+
@override
462+
void didUpdateWidget(VideoPlayer oldWidget) {
463+
super.didUpdateWidget(oldWidget);
464+
oldWidget.controller.removeListener(_listener);
465+
_textureId = widget.controller.textureId;
466+
widget.controller.addListener(_listener);
467+
}
468+
469+
@override
470+
void deactivate() {
471+
super.deactivate();
472+
widget.controller.removeListener(_listener);
449473
}
450474

451475
@override
452476
Widget build(BuildContext context) {
453-
return textureId == null
477+
return _textureId == null
454478
? new Container()
455-
: new Texture(textureId: textureId);
479+
: new Texture(textureId: _textureId);
456480
}
457481
}
458482

packages/video_player/pubspec.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player
22
description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android and iOS.
44
author: Flutter Team <flutter-dev@googlegroups.com>
5-
version: 0.6.5
5+
version: 0.7.0
66
homepage: https://github.com/flutter/plugins/tree/master/packages/video_player
77

88
flutter:
@@ -16,6 +16,10 @@ dependencies:
1616
flutter:
1717
sdk: flutter
1818

19+
dev_dependencies:
20+
flutter_test:
21+
sdk: flutter
22+
1923
environment:
2024
sdk: ">=2.0.0-dev.28.0 <3.0.0"
2125
flutter: ">=0.2.5 <2.0.0"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'dart:async';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/widgets.dart';
4+
import 'package:video_player/video_player.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
7+
class FakeController extends ValueNotifier<VideoPlayerValue>
8+
implements VideoPlayerController {
9+
FakeController() : super(new VideoPlayerValue(duration: null));
10+
11+
@override
12+
Future<void> dispose() async {
13+
super.dispose();
14+
}
15+
16+
@override
17+
int textureId;
18+
19+
@override
20+
String get dataSource => '';
21+
@override
22+
DataSourceType get dataSourceType => DataSourceType.file;
23+
@override
24+
String get package => null;
25+
@override
26+
Future<Duration> get position async => value.position;
27+
28+
@override
29+
Future<void> seekTo(Duration moment) async {}
30+
@override
31+
Future<void> setVolume(double volume) async {}
32+
@override
33+
Future<void> initialize() async {}
34+
@override
35+
Future<void> pause() async {}
36+
@override
37+
Future<void> play() async {}
38+
@override
39+
Future<void> setLooping(bool looping) async {}
40+
}
41+
42+
void main() {
43+
testWidgets('update texture', (WidgetTester tester) async {
44+
final FakeController controller = FakeController();
45+
await tester.pumpWidget(VideoPlayer(controller));
46+
expect(find.byType(Texture), findsNothing);
47+
48+
controller.textureId = 123;
49+
controller.value = controller.value.copyWith(
50+
duration: new Duration(milliseconds: 100),
51+
);
52+
53+
await tester.pump();
54+
expect(find.byType(Texture), findsOneWidget);
55+
});
56+
57+
testWidgets('update controller', (WidgetTester tester) async {
58+
final FakeController controller1 = FakeController();
59+
controller1.textureId = 101;
60+
await tester.pumpWidget(VideoPlayer(controller1));
61+
expect(
62+
find.byWidgetPredicate(
63+
(Widget widget) => widget is Texture && widget.textureId == 101,
64+
),
65+
findsOneWidget);
66+
67+
final FakeController controller2 = FakeController();
68+
controller2.textureId = 102;
69+
await tester.pumpWidget(VideoPlayer(controller2));
70+
expect(
71+
find.byWidgetPredicate(
72+
(Widget widget) => widget is Texture && widget.textureId == 102,
73+
),
74+
findsOneWidget);
75+
});
76+
}

0 commit comments

Comments
 (0)