video_demo.dart 11.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:io';
7
import 'package:connectivity/connectivity.dart';
8
import 'package:flutter/foundation.dart';
9 10
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
11
import 'package:device_info/device_info.dart';
12 13

class VideoCard extends StatelessWidget {
14
  const VideoCard({ Key key, this.controller, this.title, this.subtitle }) : super(key: key);
15

16 17 18 19 20
  final VideoPlayerController controller;
  final String title;
  final String subtitle;

  Widget _buildInlineVideo() {
21
    return Padding(
22
      padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
23 24
      child: Center(
        child: AspectRatio(
25
          aspectRatio: 3 / 2,
26
          child: Hero(
27
            tag: controller,
28
            child: VideoPlayerLoading(controller),
29 30 31 32 33 34 35
          ),
        ),
      ),
    );
  }

  Widget _buildFullScreenVideo() {
36 37 38
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
39
      ),
40 41
      body: Center(
        child: AspectRatio(
42
          aspectRatio: 3 / 2,
43
          child: Hero(
44
            tag: controller,
45
            child: VideoPlayPause(controller),
46 47 48 49 50 51 52 53
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
54 55 56 57 58
    Widget fullScreenRoutePageBuilder(
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) {
59
      return _buildFullScreenVideo();
60 61 62
    }

    void pushFullScreenWidget() {
63
      final TransitionRoute<void> route = PageRouteBuilder<void>(
64
        settings: RouteSettings(name: title),
65 66 67
        pageBuilder: fullScreenRoutePageBuilder,
      );

68
      route.completed.then((void value) {
69 70
        controller.setVolume(0.0);
      });
71 72

      controller.setVolume(1.0);
73 74 75
      Navigator.of(context).push(route);
    }

76
    return SafeArea(
77 78
      top: false,
      bottom: false,
79 80
      child: Card(
        child: Column(
81
          children: <Widget>[
82 83
            ListTile(title: Text(title), subtitle: Text(subtitle)),
            GestureDetector(
84 85 86 87 88
              onTap: pushFullScreenWidget,
              child: _buildInlineVideo(),
            ),
          ],
        ),
89 90 91 92 93
      ),
    );
  }
}

94 95 96
class VideoPlayerLoading extends StatefulWidget {
  const VideoPlayerLoading(this.controller);

97 98
  final VideoPlayerController controller;

99
  @override
100
  _VideoPlayerLoadingState createState() => _VideoPlayerLoadingState();
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
}

class _VideoPlayerLoadingState extends State<VideoPlayerLoading> {
  bool _initialized;

  @override
  void initState() {
    super.initState();
    _initialized = widget.controller.value.initialized;
    widget.controller.addListener(() {
      if (!mounted) {
        return;
      }
      final bool controllerInitialized = widget.controller.value.initialized;
      if (_initialized != controllerInitialized) {
        setState(() {
          _initialized = controllerInitialized;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_initialized) {
126
      return VideoPlayer(widget.controller);
127
    }
128
    return Stack(
129
      children: <Widget>[
130
        VideoPlayer(widget.controller),
131
        const Center(child: CircularProgressIndicator()),
132 133 134 135 136 137
      ],
      fit: StackFit.expand,
    );
  }
}

138 139 140
class VideoPlayPause extends StatefulWidget {
  const VideoPlayPause(this.controller);

141 142
  final VideoPlayerController controller;

143
  @override
144
  State createState() => _VideoPlayPauseState();
145 146 147 148 149
}

class _VideoPlayPauseState extends State<VideoPlayPause> {
  _VideoPlayPauseState() {
    listener = () {
150
      if (mounted)
151
        setState(() { });
152 153 154
    };
  }

155 156 157
  FadeAnimation imageFadeAnimation;
  VoidCallback listener;

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  VideoPlayerController get controller => widget.controller;

  @override
  void initState() {
    super.initState();
    controller.addListener(listener);
  }

  @override
  void deactivate() {
    controller.removeListener(listener);
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {
174
    return Stack(
175
      alignment: Alignment.bottomCenter,
176 177
      fit: StackFit.expand,
      children: <Widget>[
178 179
        GestureDetector(
          child: VideoPlayerLoading(controller),
180 181 182 183 184
          onTap: () {
            if (!controller.value.initialized) {
              return;
            }
            if (controller.value.isPlaying) {
185
              imageFadeAnimation = const FadeAnimation(
186
                child: Icon(Icons.pause, size: 100.0),
187 188 189
              );
              controller.pause();
            } else {
190
              imageFadeAnimation = const FadeAnimation(
191
                child: Icon(Icons.play_arrow, size: 100.0),
192 193 194 195 196
              );
              controller.play();
            }
          },
        ),
197
        Center(child: imageFadeAnimation),
198
      ],
199 200 201 202 203 204 205
    );
  }
}

class FadeAnimation extends StatefulWidget {
  const FadeAnimation({
    this.child,
206
    this.duration = const Duration(milliseconds: 500),
207 208
  });

209 210 211
  final Widget child;
  final Duration duration;

212
  @override
213
  _FadeAnimationState createState() => _FadeAnimationState();
214 215
}

216
class _FadeAnimationState extends State<FadeAnimation> with SingleTickerProviderStateMixin {
217 218 219 220 221
  AnimationController animationController;

  @override
  void initState() {
    super.initState();
222
    animationController = AnimationController(
223 224 225 226 227
      duration: widget.duration,
      vsync: this,
    );
    animationController.addListener(() {
      if (mounted) {
228
        setState(() { });
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
      }
    });
    animationController.forward(from: 0.0);
  }

  @override
  void deactivate() {
    animationController.stop();
    super.deactivate();
  }

  @override
  void didUpdateWidget(FadeAnimation oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.child != widget.child) {
      animationController.forward(from: 0.0);
    }
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return animationController.isAnimating
257
        ? Opacity(
258 259 260
            opacity: 1.0 - animationController.value,
            child: widget.child,
          )
261
        : Container();
262 263 264 265 266 267 268 269 270 271
  }
}

class ConnectivityOverlay extends StatefulWidget {
  const ConnectivityOverlay({
    this.child,
    this.connectedCompleter,
    this.scaffoldKey,
  });

272
  final Widget child;
273
  final Completer<void> connectedCompleter;
274 275
  final GlobalKey<ScaffoldState> scaffoldKey;

276
  @override
277
  _ConnectivityOverlayState createState() => _ConnectivityOverlayState();
278 279 280 281 282 283
}

class _ConnectivityOverlayState extends State<ConnectivityOverlay> {
  StreamSubscription<ConnectivityResult> connectivitySubscription;
  bool connected = true;

284
  static const SnackBar errorSnackBar = SnackBar(
285
    backgroundColor: Colors.red,
286 287 288
    content: ListTile(
      title: Text('No network'),
      subtitle: Text(
289 290 291 292 293 294
        'To load the videos you must have an active network connection',
      ),
    ),
  );

  Stream<ConnectivityResult> connectivityStream() async* {
295
    final Connectivity connectivity = Connectivity();
296 297
    ConnectivityResult previousResult = await connectivity.checkConnectivity();
    yield previousResult;
298
    await for (final ConnectivityResult result in connectivity.onConnectivityChanged) {
299 300 301 302 303 304 305 306 307 308
      if (result != previousResult) {
        yield result;
        previousResult = result;
      }
    }
  }

  @override
  void initState() {
    super.initState();
309 310 311 312 313 314 315 316 317
    if (kIsWeb) {
      // Assume connectivity
      // TODO(ditman): Remove this shortcut when `connectivity` support for web
      // lands, https://github.com/flutter/flutter/issues/46735
      if (!widget.connectedCompleter.isCompleted) {
        widget.connectedCompleter.complete(null);
      }
      return;
    }
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
    connectivitySubscription = connectivityStream().listen(
      (ConnectivityResult connectivityResult) {
        if (!mounted) {
          return;
        }
        if (connectivityResult == ConnectivityResult.none) {
          widget.scaffoldKey.currentState.showSnackBar(errorSnackBar);
        } else {
          if (!widget.connectedCompleter.isCompleted) {
            widget.connectedCompleter.complete(null);
          }
        }
      },
    );
  }

  @override
  void dispose() {
336
    connectivitySubscription?.cancel();
337 338 339 340 341 342 343 344
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

class VideoDemo extends StatefulWidget {
345
  const VideoDemo({ Key key }) : super(key: key);
346 347 348 349

  static const String routeName = '/video';

  @override
350
  _VideoDemoState createState() => _VideoDemoState();
351 352
}

353
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
354 355

Future<bool> isIOSSimulator() async {
356 357 358
  return !kIsWeb &&
      Platform.isIOS &&
      !(await deviceInfoPlugin.iosInfo).isPhysicalDevice;
359 360
}

361 362 363 364
class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
  final VideoPlayerController butterflyController = VideoPlayerController.asset(
    'videos/butterfly.mp4',
    package: 'flutter_gallery_assets',
365 366
  );

367 368 369 370
  // TODO(sigurdm): This should not be stored here.
  static const String beeUri = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
  final VideoPlayerController beeController = VideoPlayerController.network(beeUri);

371
  final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
372
  final Completer<void> connectedCompleter = Completer<void>();
373
  bool isSupported = true;
374
  bool isDisposed = false;
375 376 377 378 379

  @override
  void initState() {
    super.initState();

380 381
    Future<void> initController(VideoPlayerController controller, String name) async {
      print('> VideoDemo initController "$name" ${isDisposed ? "DISPOSED" : ""}');
382 383 384 385 386
      controller.setLooping(true);
      controller.setVolume(0.0);
      controller.play();
      await connectedCompleter.future;
      await controller.initialize();
387 388
      if (mounted) {
        print('< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}');
389
        setState(() { });
390
      }
391 392
    }

393 394
    initController(butterflyController, 'butterfly');
    initController(beeController, 'bee');
395
    isIOSSimulator().then<void>((bool result) {
396 397
      isSupported = !result;
    });
398 399 400 401
  }

  @override
  void dispose() {
402 403
    print('> VideoDemo dispose');
    isDisposed  = true;
404 405
    butterflyController.dispose();
    beeController.dispose();
406
    print('< VideoDemo dispose');
407 408 409 410 411
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
412
    return Scaffold(
413
      key: scaffoldKey,
414
      appBar: AppBar(
415 416
        title: const Text('Videos'),
      ),
417
      body: isSupported
418
        ? ConnectivityOverlay(
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
            child: Scrollbar(
              child: ListView(
                children: <Widget>[
                  VideoCard(
                    title: 'Butterfly',
                    subtitle: '… flutters by',
                    controller: butterflyController,
                  ),
                  VideoCard(
                    title: 'Bee',
                    subtitle: '… gently buzzing',
                    controller: beeController,
                  ),
                ],
              ),
434 435 436 437 438 439 440
            ),
            connectedCompleter: connectedCompleter,
            scaffoldKey: scaffoldKey,
          )
        : const Center(
            child: Text(
              'Video playback not supported on the iOS Simulator.',
441
            ),
442
          ),
443 444 445
    );
  }
}