Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit c581be4

Browse files
authored
[video_player_web] Stop buffering when browser canPlayThrough. (#5068)
1 parent c9addb4 commit c581be4

File tree

7 files changed

+557
-224
lines changed

7 files changed

+557
-224
lines changed

packages/video_player/video_player_web/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2.0.8
2+
3+
* Ensures `buffering` state is only removed when the browser reports enough data
4+
has been buffered so that the video can likely play through without stopping
5+
(`onCanPlayThrough`). Issue [#94630](https://github.com/flutter/flutter/issues/94630).
6+
* Improves testability of the `_VideoPlayer` private class.
7+
* Ensures that tests that listen to a Stream fail "fast" (1 second max timeout).
8+
19
## 2.0.7
210

311
* Internal code cleanup for stricter analysis options.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Returns the URL to load an asset from this example app as a network source.
6+
//
7+
// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the
8+
// assets directly, https://github.com/flutter/flutter/issues/95420
9+
String getUrlForAssetAsNetworkSource(String assetKey) {
10+
return 'https://github.com/flutter/plugins/blob/'
11+
// This hash can be rolled forward to pick up newly-added assets.
12+
'cb381ced070d356799dddf24aca38ce0579d3d7b'
13+
'/packages/video_player/video_player/example/'
14+
'$assetKey'
15+
'?raw=true';
16+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:html' as html;
7+
8+
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:integration_test/integration_test.dart';
10+
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
11+
import 'package:video_player_web/src/video_player.dart';
12+
13+
void main() {
14+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
15+
16+
group('VideoPlayer', () {
17+
late html.VideoElement video;
18+
19+
setUp(() {
20+
// Never set "src" on the video, so this test doesn't hit the network!
21+
video = html.VideoElement()
22+
..controls = true
23+
..setAttribute('playsinline', 'false');
24+
});
25+
26+
testWidgets('fixes critical video element config', (WidgetTester _) async {
27+
VideoPlayer(videoElement: video).initialize();
28+
29+
expect(video.controls, isFalse,
30+
reason: 'Video is controlled through code');
31+
expect(video.getAttribute('autoplay'), 'false',
32+
reason: 'Cannot autoplay on the web');
33+
expect(video.getAttribute('playsinline'), 'true',
34+
reason: 'Needed by safari iOS');
35+
});
36+
37+
testWidgets('setVolume', (WidgetTester tester) async {
38+
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();
39+
40+
player.setVolume(0);
41+
42+
expect(video.volume, isZero, reason: 'Volume should be zero');
43+
expect(video.muted, isTrue, reason: 'muted attribute should be true');
44+
45+
expect(() {
46+
player.setVolume(-0.0001);
47+
}, throwsAssertionError, reason: 'Volume cannot be < 0');
48+
49+
expect(() {
50+
player.setVolume(1.0001);
51+
}, throwsAssertionError, reason: 'Volume cannot be > 1');
52+
});
53+
54+
testWidgets('setPlaybackSpeed', (WidgetTester tester) async {
55+
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();
56+
57+
expect(() {
58+
player.setPlaybackSpeed(-1);
59+
}, throwsAssertionError, reason: 'Playback speed cannot be < 0');
60+
61+
expect(() {
62+
player.setPlaybackSpeed(0);
63+
}, throwsAssertionError, reason: 'Playback speed cannot be == 0');
64+
});
65+
66+
testWidgets('seekTo', (WidgetTester tester) async {
67+
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();
68+
69+
expect(() {
70+
player.seekTo(const Duration(seconds: -1));
71+
}, throwsAssertionError, reason: 'Cannot seek into negative numbers');
72+
});
73+
74+
// The events tested in this group do *not* represent the actual sequence
75+
// of events from a real "video" element. They're crafted to test the
76+
// behavior of the VideoPlayer in different states with different events.
77+
group('events', () {
78+
late StreamController<VideoEvent> streamController;
79+
late VideoPlayer player;
80+
late Stream<VideoEvent> timedStream;
81+
82+
final Set<VideoEventType> bufferingEvents = <VideoEventType>{
83+
VideoEventType.bufferingStart,
84+
VideoEventType.bufferingEnd,
85+
};
86+
87+
setUp(() {
88+
streamController = StreamController<VideoEvent>();
89+
player =
90+
VideoPlayer(videoElement: video, eventController: streamController)
91+
..initialize();
92+
93+
// This stream will automatically close after 100 ms without seeing any events
94+
timedStream = streamController.stream.timeout(
95+
const Duration(milliseconds: 100),
96+
onTimeout: (EventSink<VideoEvent> sink) {
97+
sink.close();
98+
},
99+
);
100+
});
101+
102+
testWidgets('buffering dispatches only when it changes',
103+
(WidgetTester tester) async {
104+
// Take all the "buffering" events that we see during the next few seconds
105+
final Future<List<bool>> stream = timedStream
106+
.where(
107+
(VideoEvent event) => bufferingEvents.contains(event.eventType))
108+
.map((VideoEvent event) =>
109+
event.eventType == VideoEventType.bufferingStart)
110+
.toList();
111+
112+
// Simulate some events coming from the player...
113+
player.setBuffering(true);
114+
player.setBuffering(true);
115+
player.setBuffering(true);
116+
player.setBuffering(false);
117+
player.setBuffering(false);
118+
player.setBuffering(true);
119+
player.setBuffering(false);
120+
player.setBuffering(true);
121+
player.setBuffering(false);
122+
123+
final List<bool> events = await stream;
124+
125+
expect(events, hasLength(6));
126+
expect(events, <bool>[true, false, true, false, true, false]);
127+
});
128+
129+
testWidgets('canplay event does not change buffering state',
130+
(WidgetTester tester) async {
131+
// Take all the "buffering" events that we see during the next few seconds
132+
final Future<List<bool>> stream = timedStream
133+
.where(
134+
(VideoEvent event) => bufferingEvents.contains(event.eventType))
135+
.map((VideoEvent event) =>
136+
event.eventType == VideoEventType.bufferingStart)
137+
.toList();
138+
139+
player.setBuffering(true);
140+
141+
// Simulate "canplay" event...
142+
video.dispatchEvent(html.Event('canplay'));
143+
144+
final List<bool> events = await stream;
145+
146+
expect(events, hasLength(1));
147+
expect(events, <bool>[true]);
148+
});
149+
150+
testWidgets('canplaythrough event does change buffering state',
151+
(WidgetTester tester) async {
152+
// Take all the "buffering" events that we see during the next few seconds
153+
final Future<List<bool>> stream = timedStream
154+
.where(
155+
(VideoEvent event) => bufferingEvents.contains(event.eventType))
156+
.map((VideoEvent event) =>
157+
event.eventType == VideoEventType.bufferingStart)
158+
.toList();
159+
160+
player.setBuffering(true);
161+
162+
// Simulate "canplaythrough" event...
163+
video.dispatchEvent(html.Event('canplaythrough'));
164+
165+
final List<bool> events = await stream;
166+
167+
expect(events, hasLength(2));
168+
expect(events, <bool>[true, false]);
169+
});
170+
171+
testWidgets('initialized dispatches only once',
172+
(WidgetTester tester) async {
173+
// Dispatch some bogus "canplay" events from the video object
174+
video.dispatchEvent(html.Event('canplay'));
175+
video.dispatchEvent(html.Event('canplay'));
176+
video.dispatchEvent(html.Event('canplay'));
177+
178+
// Take all the "initialized" events that we see during the next few seconds
179+
final Future<List<VideoEvent>> stream = timedStream
180+
.where((VideoEvent event) =>
181+
event.eventType == VideoEventType.initialized)
182+
.toList();
183+
184+
video.dispatchEvent(html.Event('canplay'));
185+
video.dispatchEvent(html.Event('canplay'));
186+
video.dispatchEvent(html.Event('canplay'));
187+
188+
final List<VideoEvent> events = await stream;
189+
190+
expect(events, hasLength(1));
191+
expect(events[0].eventType, VideoEventType.initialized);
192+
});
193+
});
194+
});
195+
}

packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ import 'package:integration_test/integration_test.dart';
1111
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
1212
import 'package:video_player_web/video_player_web.dart';
1313

14+
import 'utils.dart';
15+
16+
// Use WebM to allow CI to run tests in Chromium.
17+
const String _videoAssetKey = 'assets/Butterfly-209.webm';
18+
1419
void main() {
1520
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1621

17-
group('VideoPlayer for Web', () {
22+
group('VideoPlayerWeb plugin (hits network)', () {
1823
late Future<int> textureId;
1924

2025
setUp(() {
@@ -23,8 +28,7 @@ void main() {
2328
.create(
2429
DataSource(
2530
sourceType: DataSourceType.network,
26-
uri:
27-
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
31+
uri: getUrlForAssetAsNetworkSource(_videoAssetKey),
2832
),
2933
)
3034
.then((int? textureId) => textureId!);
@@ -38,9 +42,9 @@ void main() {
3842
expect(
3943
VideoPlayerPlatform.instance.create(
4044
DataSource(
41-
sourceType: DataSourceType.network,
42-
uri:
43-
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'),
45+
sourceType: DataSourceType.network,
46+
uri: getUrlForAssetAsNetworkSource(_videoAssetKey),
47+
),
4448
),
4549
completion(isNonZero));
4650
});
@@ -100,9 +104,9 @@ void main() {
100104
(WidgetTester tester) async {
101105
final int videoPlayerId = (await VideoPlayerPlatform.instance.create(
102106
DataSource(
103-
sourceType: DataSourceType.network,
104-
uri:
105-
'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'),
107+
sourceType: DataSourceType.network,
108+
uri: getUrlForAssetAsNetworkSource('assets/__non_existent.webm'),
109+
),
106110
))!;
107111

108112
final Stream<VideoEvent> eventStream =
@@ -113,7 +117,7 @@ void main() {
113117
await VideoPlayerPlatform.instance.play(videoPlayerId);
114118

115119
expect(() async {
116-
await eventStream.last;
120+
await eventStream.timeout(const Duration(seconds: 5)).last;
117121
}, throwsA(isA<PlatformException>()));
118122
});
119123

@@ -164,5 +168,40 @@ void main() {
164168
expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes);
165169
expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes);
166170
});
171+
172+
testWidgets('video playback lifecycle', (WidgetTester tester) async {
173+
final int videoPlayerId = await textureId;
174+
final Stream<VideoEvent> eventStream =
175+
VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId);
176+
177+
final Future<List<VideoEvent>> stream = eventStream.timeout(
178+
const Duration(seconds: 1),
179+
onTimeout: (EventSink<VideoEvent> sink) {
180+
sink.close();
181+
},
182+
).toList();
183+
184+
await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0);
185+
await VideoPlayerPlatform.instance.play(videoPlayerId);
186+
187+
// Let the video play, until we stop seeing events for a second
188+
final List<VideoEvent> events = await stream;
189+
190+
await VideoPlayerPlatform.instance.pause(videoPlayerId);
191+
192+
// The expected list of event types should look like this:
193+
// 1. bufferingStart,
194+
// 2. bufferingUpdate (videoElement.onWaiting),
195+
// 3. initialized (videoElement.onCanPlay),
196+
// 4. bufferingEnd (videoElement.onCanPlayThrough),
197+
expect(
198+
events.map((VideoEvent e) => e.eventType),
199+
equals(<VideoEventType>[
200+
VideoEventType.bufferingStart,
201+
VideoEventType.bufferingUpdate,
202+
VideoEventType.initialized,
203+
VideoEventType.bufferingEnd
204+
]));
205+
});
167206
});
168207
}

0 commit comments

Comments
 (0)