video_demo.dart 11.1 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({ super.key, this.controller, this.title, this.subtitle });
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, {super.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, {super.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 158
  FadeAnimation? imageFadeAnimation;
  late VoidCallback listener;
159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class VideoDemo extends StatefulWidget {
347
  const VideoDemo({ super.key });
348 349 350 351

  static const String routeName = '/video';

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

355
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
356 357

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

363 364 365 366
class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
  final VideoPlayerController butterflyController = VideoPlayerController.asset(
    'videos/butterfly.mp4',
    package: 'flutter_gallery_assets',
367
    videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
368 369
  );

370 371
  // TODO(sigurdm): This should not be stored here.
  static const String beeUri = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
372 373 374 375
  final VideoPlayerController beeController = VideoPlayerController.network(
    beeUri,
    videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
  );
376

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

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

385
    Future<void> initController(VideoPlayerController controller, String name) async {
386 387 388 389 390
      controller.setLooping(true);
      controller.setVolume(0.0);
      controller.play();
      await connectedCompleter.future;
      await controller.initialize();
391
      if (mounted) {
392
        setState(() { });
393
      }
394 395
    }

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

  @override
  void dispose() {
405
    isDisposed  = true;
406 407 408 409 410 411 412
    butterflyController.dispose();
    beeController.dispose();
    super.dispose();
  }

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