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