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

Commit 1f583f9

Browse files
committed
Separate _VideoPlayer to its own file. Privatize as much as possible from its API.
1 parent 04715f8 commit 1f583f9

File tree

4 files changed

+286
-217
lines changed

4 files changed

+286
-217
lines changed

packages/video_player/video_player_web/CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
## 2.0.8
22

3-
* Ensures `buffering` state is only removed when the browser reports enough video
4-
has been buffered for full playback (`onCanPlayThrough`).
5-
Issue [#94630](https://github.com/flutter/flutter/issues/94630).
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+
* Move the `VideoPlayer` "private" class to its own file, so it's directly testable
7+
without having to go through the `VideoPlayerPlugin`.
8+
* Ensure tests that listen to the Event Stream fail "fast" (5 second timeout).
69

710
## 2.0.7
811

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ String getUrlForAssetAsNetworkSource(String assetKey) {
2424
'?raw=true';
2525
}
2626

27-
// Use WebM for web to allow CI to use Chromium.
27+
// Use WebM to allow CI to run tests in Chromium.
2828
const String _videoAssetKey = 'assets/Butterfly-209.webm';
2929

3030
void main() {
@@ -128,7 +128,7 @@ void main() {
128128
await VideoPlayerPlatform.instance.play(videoPlayerId);
129129

130130
expect(() async {
131-
await eventStream.last;
131+
await eventStream.timeout(const Duration(seconds: 5)).last;
132132
}, throwsA(isA<PlatformException>()));
133133
});
134134

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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/foundation.dart' show visibleForTesting;
9+
import 'package:flutter/material.dart';
10+
import 'package:flutter/services.dart';
11+
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
12+
13+
// An error code value to error name Map.
14+
// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
15+
const Map<int, String> _kErrorValueToErrorName = <int, String>{
16+
1: 'MEDIA_ERR_ABORTED',
17+
2: 'MEDIA_ERR_NETWORK',
18+
3: 'MEDIA_ERR_DECODE',
19+
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED',
20+
};
21+
22+
// An error code value to description Map.
23+
// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
24+
const Map<int, String> _kErrorValueToErrorDescription = <int, String>{
25+
1: 'The user canceled the fetching of the video.',
26+
2: 'A network error occurred while fetching the video, despite having previously been available.',
27+
3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.',
28+
4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).',
29+
};
30+
31+
// The default error message, when the error is an empty string
32+
// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message
33+
const String _kDefaultErrorMessage =
34+
'No further diagnostic information can be determined or provided.';
35+
36+
/// Wraps a [html.VideoElement] so its API complies with what is expected by the plugin.
37+
class VideoPlayer {
38+
/// Create a [VideoPlayer] from a [html.VideoElement] instance.
39+
VideoPlayer({
40+
required html.VideoElement videoElement,
41+
@visibleForTesting StreamController<VideoEvent>? eventController,
42+
}) : _videoElement = videoElement,
43+
_eventController = eventController ?? StreamController<VideoEvent>();
44+
45+
final StreamController<VideoEvent> _eventController;
46+
final html.VideoElement _videoElement;
47+
48+
bool _isInitialized = false;
49+
bool _isBuffering = false;
50+
51+
/// Returns the [Stream] of [VideoEvent]s from the inner [html.VideoElement].
52+
Stream<VideoEvent> get events => _eventController.stream;
53+
54+
/// Initializes the wrapped [html.VideoElement].
55+
///
56+
/// This method sets the required DOM attributes so videos can [play] programmatically,
57+
/// and attaches listeners to the internal events from the [html.VideoElement]
58+
/// to react to them / expose them through the [VideoPlayer.events] stream.
59+
void initialize() {
60+
_videoElement
61+
..autoplay = false
62+
..controls = false;
63+
64+
// Allows Safari iOS to play the video inline
65+
_videoElement.setAttribute('playsinline', 'true');
66+
67+
// Set autoplay to false since most browsers won't autoplay a video unless it is muted
68+
_videoElement.setAttribute('autoplay', 'false');
69+
70+
_videoElement.onCanPlay.listen((dynamic _) {
71+
if (!_isInitialized) {
72+
_isInitialized = true;
73+
_sendInitialized();
74+
}
75+
});
76+
77+
_videoElement.onCanPlayThrough.listen((dynamic _) {
78+
_setBuffering(false);
79+
});
80+
81+
_videoElement.onPlaying.listen((dynamic _) {
82+
_setBuffering(false);
83+
});
84+
85+
_videoElement.onWaiting.listen((dynamic _) {
86+
_setBuffering(true);
87+
_sendBufferingRangesUpdate();
88+
});
89+
90+
// The error event fires when some form of error occurs while attempting to load or perform the media.
91+
_videoElement.onError.listen((html.Event _) {
92+
_setBuffering(false);
93+
// The Event itself (_) doesn't contain info about the actual error.
94+
// We need to look at the HTMLMediaElement.error.
95+
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
96+
final html.MediaError error = _videoElement.error!;
97+
_eventController.addError(PlatformException(
98+
code: _kErrorValueToErrorName[error.code]!,
99+
message: error.message != '' ? error.message : _kDefaultErrorMessage,
100+
details: _kErrorValueToErrorDescription[error.code],
101+
));
102+
});
103+
104+
_videoElement.onEnded.listen((dynamic _) {
105+
_setBuffering(false);
106+
_eventController.add(VideoEvent(eventType: VideoEventType.completed));
107+
});
108+
}
109+
110+
/// Attempts to play the video.
111+
///
112+
/// If this method is called programmatically (without user interaction), it
113+
/// might fail unless the video is completely muted (or it has no Audio tracks).
114+
///
115+
/// When called from some user interaction (a tap on a button), the above
116+
/// limitation should disappear.
117+
Future<void> play() {
118+
return _videoElement.play().catchError((Object e) {
119+
// play() attempts to begin playback of the media. It returns
120+
// a Promise which can get rejected in case of failure to begin
121+
// playback for any reason, such as permission issues.
122+
// The rejection handler is called with a DomException.
123+
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play
124+
final html.DomException exception = e as html.DomException;
125+
_eventController.addError(PlatformException(
126+
code: exception.name,
127+
message: exception.message,
128+
));
129+
}, test: (Object e) => e is html.DomException);
130+
}
131+
132+
/// Pauses the video in the current position.
133+
void pause() {
134+
_videoElement.pause();
135+
}
136+
137+
/// Controls whether the video should start again after it finishes.
138+
void setLooping(bool value) {
139+
_videoElement.loop = value;
140+
}
141+
142+
/// Sets the volume at which the media will be played.
143+
///
144+
/// Values must fall between 0 and 1, where 0 is muted and 1 is the loudest.
145+
///
146+
/// When volume is set to 0, the `muted` property is also applied to the
147+
/// [html.VideoElement]. This is required for auto-play on the web.
148+
void setVolume(double volume) {
149+
assert(volume >= 0 && volume <= 1);
150+
151+
// TODO(ditman): Do we need to expose a "muted" API?
152+
// https://github.com/flutter/flutter/issues/60721
153+
_videoElement.muted = !(volume > 0.0);
154+
_videoElement.volume = volume;
155+
}
156+
157+
/// Sets the playback `speed`.
158+
///
159+
/// A `speed` of 1.0 is "normal speed," values lower than 1.0 make the media
160+
/// play slower than normal, higher values make it play faster.
161+
///
162+
/// `speed` cannot be negative.
163+
///
164+
/// The audio is muted when the fast forward or slow motion is outside a useful
165+
/// range (for example, Gecko mutes the sound outside the range 0.25 to 4.0).
166+
///
167+
/// The pitch of the audio is corrected by default.
168+
void setPlaybackSpeed(double speed) {
169+
assert(speed > 0);
170+
171+
_videoElement.playbackRate = speed;
172+
}
173+
174+
/// Moves the playback head to a new `position`.
175+
///
176+
/// `position` cannot be negative.
177+
void seekTo(Duration position) {
178+
assert(!position.isNegative);
179+
180+
_videoElement.currentTime = position.inMilliseconds.toDouble() / 1000;
181+
}
182+
183+
/// Returns the current playback head position as a [Duration].
184+
Duration getPosition() {
185+
_sendBufferingRangesUpdate();
186+
return Duration(milliseconds: (_videoElement.currentTime * 1000).round());
187+
}
188+
189+
/// Disposes of the current [html.VideoElement].
190+
void dispose() {
191+
_videoElement.removeAttribute('src');
192+
_videoElement.load();
193+
}
194+
195+
// Sends an [VideoEventType.initialized] [VideoEvent] with info about the wrapped video.
196+
void _sendInitialized() {
197+
_eventController.add(
198+
VideoEvent(
199+
eventType: VideoEventType.initialized,
200+
duration: Duration(
201+
milliseconds: (_videoElement.duration * 1000).round(),
202+
),
203+
size: Size(
204+
_videoElement.videoWidth.toDouble(),
205+
_videoElement.videoHeight.toDouble(),
206+
),
207+
),
208+
);
209+
}
210+
211+
// Caches the current "buffering" state of the video.
212+
//
213+
// If the current buffering state is different from the previous one
214+
// ([_isBuffering]), this dispatches a [VideoEvent].
215+
void _setBuffering(bool buffering) {
216+
if (_isBuffering != buffering) {
217+
_isBuffering = buffering;
218+
_eventController.add(VideoEvent(
219+
eventType: _isBuffering
220+
? VideoEventType.bufferingStart
221+
: VideoEventType.bufferingEnd));
222+
}
223+
}
224+
225+
// Broadcasts the [html.VideoElement.buffered] status through the [events] stream.
226+
void _sendBufferingRangesUpdate() {
227+
_eventController.add(VideoEvent(
228+
buffered: _toDurationRange(_videoElement.buffered),
229+
eventType: VideoEventType.bufferingUpdate,
230+
));
231+
}
232+
233+
// Converts from [html.TimeRanges] to our own List<DurationRange>.
234+
List<DurationRange> _toDurationRange(html.TimeRanges buffered) {
235+
final List<DurationRange> durationRange = <DurationRange>[];
236+
for (int i = 0; i < buffered.length; i++) {
237+
durationRange.add(DurationRange(
238+
Duration(milliseconds: (buffered.start(i) * 1000).round()),
239+
Duration(milliseconds: (buffered.end(i) * 1000).round()),
240+
));
241+
}
242+
return durationRange;
243+
}
244+
}

0 commit comments

Comments
 (0)