Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
// Copyright 2013 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
// This file is used to extract code samples for the README.md file. | |
// Run update-excerpts if you modify this file. | |
// ignore_for_file: library_private_types_in_public_api, public_member_api_docs | |
// #docregion basic-example | |
import 'dart:io'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:video_player/video_player.dart'; | |
import 'package:fvp/fvp.dart'; | |
import 'package:logging/logging.dart'; | |
import 'package:intl/intl.dart'; | |
import 'package:file_selector/file_selector.dart'; | |
import 'package:image/image.dart' as img; | |
String? source; | |
void main(List<String> args) async { | |
// set logger before registerWith() | |
Logger.root.level = Level.ALL; | |
final df = DateFormat("HH:mm:ss.SSS"); | |
Logger.root.onRecord.listen((record) { | |
debugPrint( | |
'${record.loggerName}.${record.level.name}: ${df.format(record.time)}: ${record.message}', | |
wrapWidth: 0x7FFFFFFFFFFFFFFF); | |
}); | |
final opts = <String, Object>{}; | |
final globalOpts = <String, Object>{}; | |
int i = 0; | |
bool useFvp = true; | |
opts['subtitleFontFile'] = | |
'https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf'; | |
for (; i < args.length; i++) { | |
if (args[i] == '-c:v') { | |
opts['video.decoders'] = [args[++i]]; | |
} else if (args[i] == '-maxSize') { | |
// ${w}x${h} | |
final size = args[++i].split('x'); | |
opts['maxWidth'] = int.parse(size[0]); | |
opts['maxHeight'] = int.parse(size[1]); | |
} else if (args[i] == '-fvp') { | |
useFvp = int.parse(args[++i]) > 0; | |
} else if (args[i].startsWith('-')) { | |
globalOpts[args[i].substring(1)] = args[++i]; | |
} else { | |
break; | |
} | |
} | |
if (globalOpts.isNotEmpty) { | |
opts['global'] = globalOpts; | |
} | |
opts['lowLatency'] = 0; | |
if (i <= args.length - 1) source = args[args.length - 1]; | |
source ??= await getStartFile(); | |
if (useFvp) { | |
registerWith(options: opts); | |
} | |
runApp(const MaterialApp( | |
localizationsDelegates: [DefaultMaterialLocalizations.delegate], | |
title: 'Video Demo', | |
home: VideoApp())); | |
} | |
Future<String?> getStartFile() async { | |
if (Platform.isMacOS) { | |
WidgetsFlutterBinding.ensureInitialized(); | |
const hostApi = MethodChannel("openFile"); | |
return await hostApi.invokeMethod("getCurrentFile"); | |
} | |
return null; | |
} | |
Future<String?> showURIPicker(BuildContext context) async { | |
final key = GlobalKey<FormState>(); | |
final src = TextEditingController(); | |
String? uri; | |
await showModalBottomSheet( | |
context: context, | |
builder: (context) => Container( | |
alignment: Alignment.center, | |
child: Form( | |
key: key, | |
child: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.max, | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
TextFormField( | |
controller: src, | |
style: const TextStyle(fontSize: 14.0), | |
decoration: const InputDecoration( | |
border: UnderlineInputBorder(), | |
labelText: 'Video URI', | |
), | |
validator: (value) { | |
if (value == null || value.isEmpty) { | |
return 'Please enter a URI'; | |
} | |
return null; | |
}, | |
), | |
Padding( | |
padding: const EdgeInsets.symmetric(vertical: 16.0), | |
child: ElevatedButton( | |
onPressed: () { | |
if (key.currentState!.validate()) { | |
uri = src.text; | |
Navigator.of(context).maybePop(); | |
} | |
}, | |
child: const Text('Play'), | |
), | |
), | |
], | |
), | |
), | |
), | |
), | |
); | |
return uri; | |
} | |
/// Stateful widget to fetch and then display video content. | |
class VideoApp extends StatefulWidget { | |
const VideoApp({super.key}); | |
_VideoAppState createState() => _VideoAppState(); | |
} | |
class _VideoAppState extends State<VideoApp> { | |
late VideoPlayerController _controller; | |
void initState() { | |
super.initState(); | |
if (source != null) { | |
if (File(source!).existsSync()) { | |
playFile(source!); | |
} else { | |
playUri(source!); | |
} | |
} else { | |
playUri( | |
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'); | |
} | |
} | |
void playFile(String path) { | |
_controller.dispose(); | |
_controller = VideoPlayerController.file(File(path)); | |
_controller.addListener(() { | |
setState(() {}); | |
}); | |
_controller.initialize().then((_) { | |
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. | |
setState(() {}); | |
_controller.play(); | |
}); | |
} | |
void playUri(String uri) { | |
_controller = VideoPlayerController.network(uri); | |
_controller.addListener(() { | |
setState(() {}); | |
}); | |
_controller.initialize().then((_) { | |
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. | |
setState(() {}); | |
_controller.play(); | |
}); | |
} | |
Widget build(BuildContext context) { | |
return Scaffold( | |
floatingActionButton: Row( | |
children: [ | |
FloatingActionButton( | |
heroTag: 'file', | |
tooltip: 'Open [File]', | |
onPressed: () async { | |
const XTypeGroup typeGroup = XTypeGroup( | |
label: 'videos', | |
//extensions: <String>['*'], | |
); | |
final XFile? file = | |
await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]); | |
if (file != null) { | |
playFile(file.path); | |
} | |
}, | |
child: const Icon(Icons.file_open), | |
), | |
const SizedBox(width: 16.0), | |
FloatingActionButton( | |
heroTag: 'uri', | |
tooltip: 'Open [Uri]', | |
onPressed: () { | |
showURIPicker(context).then((value) { | |
if (value != null) { | |
playUri(value); | |
} | |
}); | |
}, | |
child: const Icon(Icons.link), | |
), | |
const SizedBox(width: 16.0), | |
FloatingActionButton( | |
heroTag: 'snapshot', | |
tooltip: 'Snapshot', | |
onPressed: () { | |
if (_controller.value.isInitialized) { | |
final info = _controller.getMediaInfo()?.video?[0].codec; | |
if (info == null) { | |
debugPrint('No video codec info'); | |
return; | |
} | |
final width = info.width; | |
final height = info.height; | |
_controller.snapshot().then((value) { | |
if (value != null) { | |
// value is rgba data, must encode to png image and save as a file | |
final i = img.Image.fromBytes( | |
width: width, | |
height: height, | |
bytes: value.buffer, | |
numChannels: 4, | |
rowStride: width * 4); | |
final savePath = | |
'${Directory.systemTemp.path}/snapshot.jpg'; | |
img.encodeJpgFile(savePath, i, quality: 70).then((value) { | |
final msg = value | |
? 'Snapshot saved to $savePath' | |
: 'Failed to save snapshot'; | |
debugPrint(msg); | |
// show a toast | |
if (context.mounted) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text(msg), | |
duration: const Duration(seconds: 2), | |
), | |
); | |
} | |
}); | |
} | |
}); | |
} | |
}, | |
child: const Icon(Icons.screenshot), | |
), | |
], | |
), | |
body: Center( | |
child: _controller.value.isInitialized | |
? AspectRatio( | |
aspectRatio: _controller.value.aspectRatio, | |
child: Stack( | |
alignment: Alignment.bottomCenter, | |
children: <Widget>[ | |
VideoPlayer(_controller), | |
_ControlsOverlay(controller: _controller), | |
VideoProgressIndicator(_controller, allowScrubbing: true), | |
], | |
), | |
) | |
: Container(), | |
), | |
); | |
} | |
void dispose() { | |
super.dispose(); | |
_controller.dispose(); | |
} | |
} | |
class _ControlsOverlay extends StatelessWidget { | |
const _ControlsOverlay({required this.controller}); | |
static const List<double> _examplePlaybackRates = <double>[ | |
0.25, | |
0.5, | |
1.0, | |
1.5, | |
2.0, | |
3.0, | |
5.0, | |
10.0, | |
]; | |
final VideoPlayerController controller; | |
Widget build(BuildContext context) { | |
return Stack( | |
children: <Widget>[ | |
AnimatedSwitcher( | |
duration: const Duration(milliseconds: 50), | |
reverseDuration: const Duration(milliseconds: 200), | |
child: controller.value.isPlaying | |
? const SizedBox.shrink() | |
: Container( | |
color: Colors.black26, | |
child: const Center( | |
child: Icon( | |
Icons.play_arrow, | |
color: Colors.white, | |
size: 100.0, | |
semanticLabel: 'Play', | |
), | |
), | |
), | |
), | |
GestureDetector( | |
onTap: () { | |
controller.value.isPlaying ? controller.pause() : controller.play(); | |
}, | |
), | |
Align( | |
alignment: Alignment.topRight, | |
child: PopupMenuButton<double>( | |
initialValue: controller.value.playbackSpeed, | |
tooltip: 'Playback speed', | |
onSelected: (double speed) { | |
controller.setPlaybackSpeed(speed); | |
}, | |
itemBuilder: (BuildContext context) { | |
return <PopupMenuItem<double>>[ | |
for (final double speed in _examplePlaybackRates) | |
PopupMenuItem<double>( | |
value: speed, | |
child: Text('${speed}x'), | |
) | |
]; | |
}, | |
child: Padding( | |
padding: const EdgeInsets.symmetric( | |
// Using less vertical padding as the text is also longer | |
// horizontally, so it feels like it would need more spacing | |
// horizontally (matching the aspect ratio of the video). | |
vertical: 12, | |
horizontal: 16, | |
), | |
child: Text('${controller.value.playbackSpeed}x'), | |
), | |
), | |
), | |
], | |
); | |
} | |
} | |
// #enddocregion basic-example | |