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

[video_player_web] Stop buffering when browser canPlayThrough. #5068

Merged
merged 11 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions packages/video_player/video_player_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2.0.8

* Ensures `buffering` state is only removed when the browser reports enough data
has been buffered so that the video can likely play through without stopping
(`onCanPlayThrough`). Issue [#94630](https://github.com/flutter/flutter/issues/94630).
* Improves testability of the `_VideoPlayer` private class.
* Ensures that tests that listen to a Stream fail "fast" (1 second max timeout).

## 2.0.7

* Internal code cleanup for stricter analysis options.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Returns the URL to load an asset from this example app as a network source.
//
// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the
// assets directly, https://github.com/flutter/flutter/issues/95420
String getUrlForAssetAsNetworkSource(String assetKey) {
return 'https://github.com/flutter/plugins/blob/'
// This hash can be rolled forward to pick up newly-added assets.
'cb381ced070d356799dddf24aca38ce0579d3d7b'
'/packages/video_player/video_player/example/'
'$assetKey'
'?raw=true';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:html' as html;

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
import 'package:video_player_web/src/video_player.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('VideoPlayer', () {
late html.VideoElement video;

setUp(() {
// Never set "src" on the video, so this test doesn't hit the network!
video = html.VideoElement()
..controls = true
..setAttribute('playsinline', 'false');
});

testWidgets('fixes critical video element config', (WidgetTester _) async {
VideoPlayer(videoElement: video).initialize();

expect(video.controls, isFalse,
reason: 'Video is controlled through code');
expect(video.getAttribute('autoplay'), 'false',
reason: 'Cannot autoplay on the web');
expect(video.getAttribute('playsinline'), 'true',
reason: 'Needed by safari iOS');
});

testWidgets('setVolume', (WidgetTester tester) async {
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();

player.setVolume(0);

expect(video.volume, isZero, reason: 'Volume should be zero');
expect(video.muted, isTrue, reason: 'muted attribute should be true');

expect(() {
player.setVolume(-0.0001);
}, throwsAssertionError, reason: 'Volume cannot be < 0');

expect(() {
player.setVolume(1.0001);
}, throwsAssertionError, reason: 'Volume cannot be > 1');
});

testWidgets('setPlaybackSpeed', (WidgetTester tester) async {
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();

expect(() {
player.setPlaybackSpeed(-1);
}, throwsAssertionError, reason: 'Playback speed cannot be < 0');

expect(() {
player.setPlaybackSpeed(0);
}, throwsAssertionError, reason: 'Playback speed cannot be == 0');
});

testWidgets('seekTo', (WidgetTester tester) async {
final VideoPlayer player = VideoPlayer(videoElement: video)..initialize();

expect(() {
player.seekTo(const Duration(seconds: -1));
}, throwsAssertionError, reason: 'Cannot seek into negative numbers');
});

// The events tested in this group do *not* represent the actual sequence
// of events from a real "video" element. They're crafted to test the
// behavior of the VideoPlayer in different states with different events.
group('events', () {
late StreamController<VideoEvent> streamController;
late VideoPlayer player;
late Stream<VideoEvent> timedStream;

final Set<VideoEventType> bufferingEvents = <VideoEventType>{
VideoEventType.bufferingStart,
VideoEventType.bufferingEnd,
};

setUp(() {
streamController = StreamController<VideoEvent>();
player =
VideoPlayer(videoElement: video, eventController: streamController)
..initialize();

// This stream will automatically close after 100 ms without seeing any events
timedStream = streamController.stream.timeout(
const Duration(milliseconds: 100),
onTimeout: (EventSink<VideoEvent> sink) {
sink.close();
},
);
});

testWidgets('buffering dispatches only when it changes',
(WidgetTester tester) async {
// Take all the "buffering" events that we see during the next few seconds
final Future<List<bool>> stream = timedStream
.where(
(VideoEvent event) => bufferingEvents.contains(event.eventType))
.map((VideoEvent event) =>
event.eventType == VideoEventType.bufferingStart)
.toList();

// Simulate some events coming from the player...
player.setBuffering(true);
player.setBuffering(true);
player.setBuffering(true);
player.setBuffering(false);
player.setBuffering(false);
player.setBuffering(true);
player.setBuffering(false);
player.setBuffering(true);
player.setBuffering(false);

final List<bool> events = await stream;

expect(events, hasLength(6));
expect(events, <bool>[true, false, true, false, true, false]);
});

testWidgets('canplay event does not change buffering state',
(WidgetTester tester) async {
// Take all the "buffering" events that we see during the next few seconds
final Future<List<bool>> stream = timedStream
.where(
(VideoEvent event) => bufferingEvents.contains(event.eventType))
.map((VideoEvent event) =>
event.eventType == VideoEventType.bufferingStart)
.toList();

player.setBuffering(true);

// Simulate "canplay" event...
video.dispatchEvent(html.Event('canplay'));

final List<bool> events = await stream;

expect(events, hasLength(1));
expect(events, <bool>[true]);
});

testWidgets('canplaythrough event does change buffering state',
(WidgetTester tester) async {
// Take all the "buffering" events that we see during the next few seconds
final Future<List<bool>> stream = timedStream
.where(
(VideoEvent event) => bufferingEvents.contains(event.eventType))
.map((VideoEvent event) =>
event.eventType == VideoEventType.bufferingStart)
.toList();

player.setBuffering(true);

// Simulate "canplaythrough" event...
video.dispatchEvent(html.Event('canplaythrough'));

final List<bool> events = await stream;

expect(events, hasLength(2));
expect(events, <bool>[true, false]);
});

testWidgets('initialized dispatches only once',
(WidgetTester tester) async {
// Dispatch some bogus "canplay" events from the video object
video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(html.Event('canplay'));

// Take all the "initialized" events that we see during the next few seconds
final Future<List<VideoEvent>> stream = timedStream
.where((VideoEvent event) =>
event.eventType == VideoEventType.initialized)
.toList();

video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(html.Event('canplay'));
video.dispatchEvent(html.Event('canplay'));

final List<VideoEvent> events = await stream;

expect(events, hasLength(1));
expect(events[0].eventType, VideoEventType.initialized);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import 'package:integration_test/integration_test.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
import 'package:video_player_web/video_player_web.dart';

import 'utils.dart';

// Use WebM to allow CI to run tests in Chromium.
const String _videoAssetKey = 'assets/Butterfly-209.webm';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('VideoPlayer for Web', () {
group('VideoPlayerWeb plugin (hits network)', () {
late Future<int> textureId;

setUp(() {
Expand All @@ -23,8 +28,7 @@ void main() {
.create(
DataSource(
sourceType: DataSourceType.network,
uri:
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
uri: getUrlForAssetAsNetworkSource(_videoAssetKey),
),
)
.then((int? textureId) => textureId!);
Expand All @@ -38,9 +42,9 @@ void main() {
expect(
VideoPlayerPlatform.instance.create(
DataSource(
sourceType: DataSourceType.network,
uri:
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'),
sourceType: DataSourceType.network,
uri: getUrlForAssetAsNetworkSource(_videoAssetKey),
),
),
completion(isNonZero));
});
Expand Down Expand Up @@ -100,9 +104,9 @@ void main() {
(WidgetTester tester) async {
final int videoPlayerId = (await VideoPlayerPlatform.instance.create(
DataSource(
sourceType: DataSourceType.network,
uri:
'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'),
sourceType: DataSourceType.network,
uri: getUrlForAssetAsNetworkSource('assets/__non_existent.webm'),
),
))!;

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

expect(() async {
await eventStream.last;
await eventStream.timeout(const Duration(seconds: 5)).last;
}, throwsA(isA<PlatformException>()));
});

Expand Down Expand Up @@ -164,5 +168,40 @@ void main() {
expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes);
expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes);
});

testWidgets('video playback lifecycle', (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: 1),
onTimeout: (EventSink<VideoEvent> sink) {
sink.close();
},
).toList();

await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0);
await VideoPlayerPlatform.instance.play(videoPlayerId);

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

await VideoPlayerPlatform.instance.pause(videoPlayerId);

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