video_demo.dart 11.4 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

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

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

17 18 19
  final VideoPlayerController? controller;
  final String? title;
  final String? subtitle;
20 21

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

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

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

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

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

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

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

95
class VideoPlayerLoading extends StatefulWidget {
96
  const VideoPlayerLoading(this.controller, {Key? key}) : super(key: key);
97

98
  final VideoPlayerController? controller;
99

100
  @override
101
  State<VideoPlayerLoading> createState() => _VideoPlayerLoadingState();
102 103 104
}

class _VideoPlayerLoadingState extends State<VideoPlayerLoading> {
105
  bool? _initialized;
106 107 108 109

  @override
  void initState() {
    super.initState();
110 111
    _initialized = widget.controller!.value.isInitialized;
    widget.controller!.addListener(() {
112 113 114
      if (!mounted) {
        return;
      }
115
      final bool controllerInitialized = widget.controller!.value.isInitialized;
116 117 118 119 120 121 122 123 124 125
      if (_initialized != controllerInitialized) {
        setState(() {
          _initialized = controllerInitialized;
        });
      }
    });
  }

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

139
class VideoPlayPause extends StatefulWidget {
140
  const VideoPlayPause(this.controller, {Key? key}) : super(key: key);
141

142
  final VideoPlayerController? controller;
143

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

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

156 157
  FadeAnimation? imageFadeAnimation;
  late VoidCallback listener;
158

159
  VideoPlayerController? get controller => widget.controller;
160 161 162 163

  @override
  void initState() {
    super.initState();
164
    controller!.addListener(listener);
165 166 167 168
  }

  @override
  void deactivate() {
169
    controller!.removeListener(listener);
170 171 172 173 174
    super.deactivate();
  }

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

class FadeAnimation extends StatefulWidget {
  const FadeAnimation({
206
    Key? key,
207
    this.child,
208
    this.duration = const Duration(milliseconds: 500),
209
  }) : super(key: key);
210

211
  final Widget? child;
212 213
  final Duration duration;

214
  @override
215
  State<FadeAnimation> createState() => _FadeAnimationState();
216 217
}

218
class _FadeAnimationState extends State<FadeAnimation> with SingleTickerProviderStateMixin {
219
  late AnimationController animationController;
220 221 222 223

  @override
  void initState() {
    super.initState();
224
    animationController = AnimationController(
225 226 227 228 229
      duration: widget.duration,
      vsync: this,
    );
    animationController.addListener(() {
      if (mounted) {
230
        setState(() { });
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 257 258
      }
    });
    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
259
        ? Opacity(
260 261 262
            opacity: 1.0 - animationController.value,
            child: widget.child,
          )
263
        : Container();
264 265 266 267 268
  }
}

class ConnectivityOverlay extends StatefulWidget {
  const ConnectivityOverlay({
269
    Key? key,
270 271
    this.child,
    this.connectedCompleter,
272
  }) : super(key: key);
273

274 275
  final Widget? child;
  final Completer<void>? connectedCompleter;
276

277
  @override
278
  State<ConnectivityOverlay> createState() => _ConnectivityOverlayState();
279 280 281
}

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

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

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

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

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

  @override
342
  Widget build(BuildContext context) => widget.child!;
343 344 345
}

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

  static const String routeName = '/video';

  @override
351
  State<VideoDemo> createState() => _VideoDemoState();
352 353
}

354
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
355 356

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

362 363 364 365
class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
  final VideoPlayerController butterflyController = VideoPlayerController.asset(
    'videos/butterfly.mp4',
    package: 'flutter_gallery_assets',
366
    videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
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';
371 372 373 374
  final VideoPlayerController beeController = VideoPlayerController.network(
    beeUri,
    videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
  );
375

376
  final Completer<void> connectedCompleter = Completer<void>();
377
  bool isSupported = true;
378
  bool isDisposed = false;
379 380 381 382 383

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

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

397 398
    initController(butterflyController, 'butterfly');
    initController(beeController, 'bee');
399
    isIOSSimulator().then((bool result) {
400 401
      isSupported = !result;
    });
402 403 404 405
  }

  @override
  void dispose() {
406 407
    print('> VideoDemo dispose');
    isDisposed  = true;
408 409
    butterflyController.dispose();
    beeController.dispose();
410
    print('< VideoDemo dispose');
411 412 413 414 415
    super.dispose();
  }

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