video_widget_new.dart 8.9 KB

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