• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium 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
5import 'dart:async';
6import 'dart:io';
7import 'package:connectivity/connectivity.dart';
8import 'package:flutter/material.dart';
9import 'package:video_player/video_player.dart';
10import 'package:device_info/device_info.dart';
11
12class VideoCard extends StatelessWidget {
13  const VideoCard({ Key key, this.controller, this.title, this.subtitle }) : super(key: key);
14
15  final VideoPlayerController controller;
16  final String title;
17  final String subtitle;
18
19  Widget _buildInlineVideo() {
20    return Padding(
21      padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
22      child: Center(
23        child: AspectRatio(
24          aspectRatio: 3 / 2,
25          child: Hero(
26            tag: controller,
27            child: VideoPlayerLoading(controller),
28          ),
29        ),
30      ),
31    );
32  }
33
34  Widget _buildFullScreenVideo() {
35    return Scaffold(
36      appBar: AppBar(
37        title: Text(title),
38      ),
39      body: Center(
40        child: AspectRatio(
41          aspectRatio: 3 / 2,
42          child: Hero(
43            tag: controller,
44            child: VideoPlayPause(controller),
45          ),
46        ),
47      ),
48    );
49  }
50
51  @override
52  Widget build(BuildContext context) {
53    Widget fullScreenRoutePageBuilder(
54      BuildContext context,
55      Animation<double> animation,
56      Animation<double> secondaryAnimation,
57    ) {
58      return _buildFullScreenVideo();
59    }
60
61    void pushFullScreenWidget() {
62      final TransitionRoute<void> route = PageRouteBuilder<void>(
63        settings: RouteSettings(name: title, isInitialRoute: false),
64        pageBuilder: fullScreenRoutePageBuilder,
65      );
66
67      route.completed.then((void value) {
68        controller.setVolume(0.0);
69      });
70
71      controller.setVolume(1.0);
72      Navigator.of(context).push(route);
73    }
74
75    return SafeArea(
76      top: false,
77      bottom: false,
78      child: Card(
79        child: Column(
80          children: <Widget>[
81            ListTile(title: Text(title), subtitle: Text(subtitle)),
82            GestureDetector(
83              onTap: pushFullScreenWidget,
84              child: _buildInlineVideo(),
85            ),
86          ],
87        ),
88      ),
89    );
90  }
91}
92
93class VideoPlayerLoading extends StatefulWidget {
94  const VideoPlayerLoading(this.controller);
95
96  final VideoPlayerController controller;
97
98  @override
99  _VideoPlayerLoadingState createState() => _VideoPlayerLoadingState();
100}
101
102class _VideoPlayerLoadingState extends State<VideoPlayerLoading> {
103  bool _initialized;
104
105  @override
106  void initState() {
107    super.initState();
108    _initialized = widget.controller.value.initialized;
109    widget.controller.addListener(() {
110      if (!mounted) {
111        return;
112      }
113      final bool controllerInitialized = widget.controller.value.initialized;
114      if (_initialized != controllerInitialized) {
115        setState(() {
116          _initialized = controllerInitialized;
117        });
118      }
119    });
120  }
121
122  @override
123  Widget build(BuildContext context) {
124    if (_initialized) {
125      return VideoPlayer(widget.controller);
126    }
127    return Stack(
128      children: <Widget>[
129        VideoPlayer(widget.controller),
130        const Center(child: CircularProgressIndicator()),
131      ],
132      fit: StackFit.expand,
133    );
134  }
135}
136
137class VideoPlayPause extends StatefulWidget {
138  const VideoPlayPause(this.controller);
139
140  final VideoPlayerController controller;
141
142  @override
143  State createState() => _VideoPlayPauseState();
144}
145
146class _VideoPlayPauseState extends State<VideoPlayPause> {
147  _VideoPlayPauseState() {
148    listener = () {
149      if (mounted)
150        setState(() { });
151    };
152  }
153
154  FadeAnimation imageFadeAnimation;
155  VoidCallback listener;
156
157  VideoPlayerController get controller => widget.controller;
158
159  @override
160  void initState() {
161    super.initState();
162    controller.addListener(listener);
163  }
164
165  @override
166  void deactivate() {
167    controller.removeListener(listener);
168    super.deactivate();
169  }
170
171  @override
172  Widget build(BuildContext context) {
173    return Stack(
174      alignment: Alignment.bottomCenter,
175      fit: StackFit.expand,
176      children: <Widget>[
177        GestureDetector(
178          child: VideoPlayerLoading(controller),
179          onTap: () {
180            if (!controller.value.initialized) {
181              return;
182            }
183            if (controller.value.isPlaying) {
184              imageFadeAnimation = const FadeAnimation(
185                child: Icon(Icons.pause, size: 100.0),
186              );
187              controller.pause();
188            } else {
189              imageFadeAnimation = const FadeAnimation(
190                child: Icon(Icons.play_arrow, size: 100.0),
191              );
192              controller.play();
193            }
194          },
195        ),
196        Center(child: imageFadeAnimation),
197      ],
198    );
199  }
200}
201
202class FadeAnimation extends StatefulWidget {
203  const FadeAnimation({
204    this.child,
205    this.duration = const Duration(milliseconds: 500),
206  });
207
208  final Widget child;
209  final Duration duration;
210
211  @override
212  _FadeAnimationState createState() => _FadeAnimationState();
213}
214
215class _FadeAnimationState extends State<FadeAnimation> with SingleTickerProviderStateMixin {
216  AnimationController animationController;
217
218  @override
219  void initState() {
220    super.initState();
221    animationController = AnimationController(
222      duration: widget.duration,
223      vsync: this,
224    );
225    animationController.addListener(() {
226      if (mounted) {
227        setState(() { });
228      }
229    });
230    animationController.forward(from: 0.0);
231  }
232
233  @override
234  void deactivate() {
235    animationController.stop();
236    super.deactivate();
237  }
238
239  @override
240  void didUpdateWidget(FadeAnimation oldWidget) {
241    super.didUpdateWidget(oldWidget);
242    if (oldWidget.child != widget.child) {
243      animationController.forward(from: 0.0);
244    }
245  }
246
247  @override
248  void dispose() {
249    animationController.dispose();
250    super.dispose();
251  }
252
253  @override
254  Widget build(BuildContext context) {
255    return animationController.isAnimating
256        ? Opacity(
257            opacity: 1.0 - animationController.value,
258            child: widget.child,
259          )
260        : Container();
261  }
262}
263
264class ConnectivityOverlay extends StatefulWidget {
265  const ConnectivityOverlay({
266    this.child,
267    this.connectedCompleter,
268    this.scaffoldKey,
269  });
270
271  final Widget child;
272  final Completer<void> connectedCompleter;
273  final GlobalKey<ScaffoldState> scaffoldKey;
274
275  @override
276  _ConnectivityOverlayState createState() => _ConnectivityOverlayState();
277}
278
279class _ConnectivityOverlayState extends State<ConnectivityOverlay> {
280  StreamSubscription<ConnectivityResult> connectivitySubscription;
281  bool connected = true;
282
283  static const Widget errorSnackBar = SnackBar(
284    backgroundColor: Colors.red,
285    content: ListTile(
286      title: Text('No network'),
287      subtitle: Text(
288        'To load the videos you must have an active network connection',
289      ),
290    ),
291  );
292
293  Stream<ConnectivityResult> connectivityStream() async* {
294    final Connectivity connectivity = Connectivity();
295    ConnectivityResult previousResult = await connectivity.checkConnectivity();
296    yield previousResult;
297    await for (ConnectivityResult result
298        in connectivity.onConnectivityChanged) {
299      if (result != previousResult) {
300        yield result;
301        previousResult = result;
302      }
303    }
304  }
305
306  @override
307  void initState() {
308    super.initState();
309    connectivitySubscription = connectivityStream().listen(
310      (ConnectivityResult connectivityResult) {
311        if (!mounted) {
312          return;
313        }
314        if (connectivityResult == ConnectivityResult.none) {
315          widget.scaffoldKey.currentState.showSnackBar(errorSnackBar);
316        } else {
317          if (!widget.connectedCompleter.isCompleted) {
318            widget.connectedCompleter.complete(null);
319          }
320        }
321      },
322    );
323  }
324
325  @override
326  void dispose() {
327    connectivitySubscription.cancel();
328    super.dispose();
329  }
330
331  @override
332  Widget build(BuildContext context) => widget.child;
333}
334
335class VideoDemo extends StatefulWidget {
336  const VideoDemo({ Key key }) : super(key: key);
337
338  static const String routeName = '/video';
339
340  @override
341  _VideoDemoState createState() => _VideoDemoState();
342}
343
344final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
345
346Future<bool> isIOSSimulator() async {
347  return Platform.isIOS && !(await deviceInfoPlugin.iosInfo).isPhysicalDevice;
348}
349
350class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
351  final VideoPlayerController butterflyController = VideoPlayerController.asset(
352    'videos/butterfly.mp4',
353    package: 'flutter_gallery_assets',
354  );
355
356  // TODO(sigurdm): This should not be stored here.
357  static const String beeUri = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
358  final VideoPlayerController beeController = VideoPlayerController.network(beeUri);
359
360  final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
361  final Completer<void> connectedCompleter = Completer<void>();
362  bool isSupported = true;
363  bool isDisposed = false;
364
365  @override
366  void initState() {
367    super.initState();
368
369    Future<void> initController(VideoPlayerController controller, String name) async {
370      print('> VideoDemo initController "$name" ${isDisposed ? "DISPOSED" : ""}');
371      controller.setLooping(true);
372      controller.setVolume(0.0);
373      controller.play();
374      await connectedCompleter.future;
375      await controller.initialize();
376      if (mounted) {
377        print('< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}');
378        setState(() { });
379      }
380    }
381
382    initController(butterflyController, 'butterfly');
383    initController(beeController, 'bee');
384    isIOSSimulator().then<void>((bool result) {
385      isSupported = !result;
386    });
387  }
388
389  @override
390  void dispose() {
391    print('> VideoDemo dispose');
392    isDisposed  = true;
393    butterflyController.dispose();
394    beeController.dispose();
395    print('< VideoDemo dispose');
396    super.dispose();
397  }
398
399  @override
400  Widget build(BuildContext context) {
401    return Scaffold(
402      key: scaffoldKey,
403      appBar: AppBar(
404        title: const Text('Videos'),
405      ),
406      body: isSupported
407        ? ConnectivityOverlay(
408            child: Scrollbar(
409              child: ListView(
410                children: <Widget>[
411                  VideoCard(
412                    title: 'Butterfly',
413                    subtitle: '… flutters by',
414                    controller: butterflyController,
415                  ),
416                  VideoCard(
417                    title: 'Bee',
418                    subtitle: '… gently buzzing',
419                    controller: beeController,
420                  ),
421                ],
422              ),
423            ),
424            connectedCompleter: connectedCompleter,
425            scaffoldKey: scaffoldKey,
426          )
427        : const Center(
428            child: Text(
429              'Video playback not supported on the iOS Simulator.',
430            ),
431          ),
432    );
433  }
434}
435