video_widget_new.dart 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import "dart:io";
  2. import "package:flutter/cupertino.dart";
  3. import "package:flutter/material.dart";
  4. import "package:media_kit/media_kit.dart";
  5. import "package:media_kit_video/media_kit_video.dart";
  6. import "package:photos/core/constants.dart";
  7. import "package:photos/generated/l10n.dart";
  8. import "package:photos/models/file/extensions/file_props.dart";
  9. import "package:photos/models/file/file.dart";
  10. import "package:photos/services/files_service.dart";
  11. import "package:photos/theme/colors.dart";
  12. import "package:photos/theme/ente_theme.dart";
  13. import "package:photos/ui/viewer/file/thumbnail_widget.dart";
  14. import "package:photos/utils/dialog_util.dart";
  15. import "package:photos/utils/file_util.dart";
  16. import "package:photos/utils/toast_util.dart";
  17. class VideoWidgetNew extends StatefulWidget {
  18. final EnteFile file;
  19. final String? tagPrefix;
  20. final Function(bool)? playbackCallback;
  21. const VideoWidgetNew(
  22. this.file, {
  23. this.tagPrefix,
  24. this.playbackCallback,
  25. super.key,
  26. });
  27. @override
  28. State<VideoWidgetNew> createState() => _VideoWidgetNewState();
  29. }
  30. class _VideoWidgetNewState extends State<VideoWidgetNew> {
  31. static const verticalMargin = 100.0;
  32. late final player = Player();
  33. VideoController? controller;
  34. final _progressNotifier = ValueNotifier<double?>(null);
  35. @override
  36. void initState() {
  37. super.initState();
  38. if (widget.file.isRemoteFile) {
  39. _loadNetworkVideo();
  40. _setFileSizeIfNull();
  41. } else if (widget.file.isSharedMediaToAppSandbox) {
  42. final localFile = File(getSharedMediaFilePath(widget.file));
  43. if (localFile.existsSync()) {
  44. _setVideoController(localFile.path);
  45. } else if (widget.file.uploadedFileID != null) {
  46. _loadNetworkVideo();
  47. }
  48. } else {
  49. widget.file.getAsset.then((asset) async {
  50. if (asset == null || !(await asset.exists)) {
  51. if (widget.file.uploadedFileID != null) {
  52. _loadNetworkVideo();
  53. }
  54. } else {
  55. asset.getMediaUrl().then((url) {
  56. _setVideoController(
  57. url ??
  58. 'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
  59. );
  60. });
  61. }
  62. });
  63. }
  64. player.stream.playing.listen((event) {
  65. if (widget.playbackCallback != null) {
  66. widget.playbackCallback!(event);
  67. }
  68. });
  69. }
  70. @override
  71. void dispose() {
  72. player.dispose();
  73. // _progressNotifier.dispose();
  74. super.dispose();
  75. }
  76. @override
  77. Widget build(BuildContext context) {
  78. final colorScheme = getEnteColorScheme(context);
  79. return Hero(
  80. tag: widget.tagPrefix! + widget.file.tag,
  81. child: MaterialVideoControlsTheme(
  82. normal: MaterialVideoControlsThemeData(
  83. backdropColor: null,
  84. automaticallyImplySkipNextButton: false,
  85. automaticallyImplySkipPreviousButton: false,
  86. seekOnDoubleTap: false,
  87. displaySeekBar: true,
  88. seekBarMargin: const EdgeInsets.only(bottom: verticalMargin),
  89. bottomButtonBarMargin: const EdgeInsets.only(bottom: 112),
  90. controlsHoverDuration: const Duration(seconds: 3),
  91. seekBarHeight: 2,
  92. seekBarThumbSize: 16,
  93. seekBarBufferColor: Colors.transparent,
  94. seekBarThumbColor: backgroundElevatedLight,
  95. seekBarColor: fillMutedDark,
  96. seekBarPositionColor: colorScheme.primary300,
  97. ///topButtonBarMargin is needed for keeping the buffering loading
  98. ///indicator to be center aligned
  99. topButtonBarMargin: const EdgeInsets.only(top: verticalMargin),
  100. bottomButtonBar: [
  101. const Spacer(),
  102. PausePlayAndDuration(controller?.player),
  103. const Spacer(),
  104. ],
  105. primaryButtonBar: [],
  106. ),
  107. fullscreen: const MaterialVideoControlsThemeData(),
  108. child: GestureDetector(
  109. onVerticalDragUpdate: (d) => {
  110. if (d.delta.dy > dragSensitivity) {Navigator.of(context).pop()},
  111. },
  112. child: Center(
  113. child: controller != null
  114. ? Video(
  115. controller: controller!,
  116. )
  117. : _getLoadingWidget(),
  118. ),
  119. ),
  120. ),
  121. );
  122. }
  123. void _loadNetworkVideo() {
  124. getFileFromServer(
  125. widget.file,
  126. progressCallback: (count, total) {
  127. _progressNotifier.value = count / (widget.file.fileSize ?? total);
  128. if (_progressNotifier.value == 1) {
  129. if (mounted) {
  130. showShortToast(context, S.of(context).decryptingVideo);
  131. }
  132. }
  133. },
  134. ).then((file) {
  135. if (file != null) {
  136. _setVideoController(file.path);
  137. }
  138. }).onError((error, stackTrace) {
  139. showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo);
  140. });
  141. }
  142. void _setFileSizeIfNull() {
  143. if (widget.file.fileSize == null && widget.file.canEditMetaInfo) {
  144. FilesService.instance
  145. .getFileSize(widget.file.uploadedFileID!)
  146. .then((value) {
  147. widget.file.fileSize = value;
  148. if (mounted) {
  149. setState(() {});
  150. }
  151. });
  152. }
  153. }
  154. Widget _getLoadingWidget() {
  155. return Stack(
  156. children: [
  157. _getThumbnail(),
  158. Container(
  159. color: Colors.black12,
  160. constraints: const BoxConstraints.expand(),
  161. ),
  162. Center(
  163. child: SizedBox.fromSize(
  164. size: const Size.square(20),
  165. child: ValueListenableBuilder(
  166. valueListenable: _progressNotifier,
  167. builder: (BuildContext context, double? progress, _) {
  168. return progress == null || progress == 1
  169. ? const CupertinoActivityIndicator(
  170. color: Colors.white,
  171. )
  172. : CircularProgressIndicator(
  173. backgroundColor: Colors.black,
  174. value: progress,
  175. valueColor: const AlwaysStoppedAnimation<Color>(
  176. Color.fromRGBO(45, 194, 98, 1.0),
  177. ),
  178. );
  179. },
  180. ),
  181. ),
  182. ),
  183. ],
  184. );
  185. }
  186. Widget _getThumbnail() {
  187. return Container(
  188. color: Colors.black,
  189. constraints: const BoxConstraints.expand(),
  190. child: ThumbnailWidget(
  191. widget.file,
  192. fit: BoxFit.contain,
  193. ),
  194. );
  195. }
  196. void _setVideoController(String url) {
  197. if (mounted) {
  198. setState(() {
  199. controller = VideoController(player);
  200. player.open(Media(url));
  201. });
  202. }
  203. }
  204. }
  205. class PausePlayAndDuration extends StatefulWidget {
  206. final Player? player;
  207. const PausePlayAndDuration(this.player, {super.key});
  208. @override
  209. State<PausePlayAndDuration> createState() => _PausePlayAndDurationState();
  210. }
  211. class _PausePlayAndDurationState extends State<PausePlayAndDuration> {
  212. Color backgroundColor = fillStrongLight;
  213. @override
  214. Widget build(BuildContext context) {
  215. return GestureDetector(
  216. onTapDown: (details) {
  217. setState(() {
  218. backgroundColor = fillMutedDark;
  219. });
  220. },
  221. onTapUp: (details) {
  222. Future.delayed(const Duration(milliseconds: 175), () {
  223. if (mounted) {
  224. setState(() {
  225. backgroundColor = fillStrongLight;
  226. });
  227. }
  228. });
  229. },
  230. onTapCancel: () {
  231. Future.delayed(const Duration(milliseconds: 175), () {
  232. if (mounted) {
  233. setState(() {
  234. backgroundColor = fillStrongLight;
  235. });
  236. }
  237. });
  238. },
  239. onTap: () => widget.player!.playOrPause(),
  240. child: AnimatedContainer(
  241. duration: const Duration(milliseconds: 150),
  242. curve: Curves.easeInBack,
  243. padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
  244. decoration: BoxDecoration(
  245. color: backgroundColor,
  246. border: Border.all(
  247. color: strokeFaintDark,
  248. width: 1,
  249. ),
  250. borderRadius: BorderRadius.circular(24),
  251. ),
  252. child: AnimatedSize(
  253. duration: const Duration(seconds: 2),
  254. curve: Curves.easeInOutExpo,
  255. child: Row(
  256. children: [
  257. StreamBuilder(
  258. builder: (context, snapshot) {
  259. final bool isPlaying = snapshot.data ?? false;
  260. return AnimatedSwitcher(
  261. duration: const Duration(milliseconds: 350),
  262. switchInCurve: Curves.easeInOutCirc,
  263. switchOutCurve: Curves.easeInOutCirc,
  264. child: Icon(
  265. key: ValueKey(
  266. isPlaying ? "pause_button" : "play_button",
  267. ),
  268. isPlaying
  269. ? Icons.pause_rounded
  270. : Icons.play_arrow_rounded,
  271. color: backdropBaseLight,
  272. size: 24,
  273. ),
  274. );
  275. },
  276. initialData: widget.player?.state.playing,
  277. stream: widget.player?.stream.playing,
  278. ),
  279. const SizedBox(width: 8),
  280. MaterialPositionIndicator(
  281. style: getEnteTextTheme(context).tiny.copyWith(
  282. color: textBaseDark,
  283. ),
  284. ),
  285. const SizedBox(width: 10),
  286. ],
  287. ),
  288. ),
  289. ),
  290. );
  291. }
  292. }