diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index b907203cf9..b2e00dfbdf 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -52,7 +52,6 @@ import "package:photos/utils/email_util.dart"; import 'package:photos/utils/file_uploader.dart'; import "package:photos/utils/lock_screen_settings.dart"; import 'package:shared_preferences/shared_preferences.dart'; -import "package:video_player_media_kit/video_player_media_kit.dart"; final _logger = Logger("main"); @@ -238,10 +237,6 @@ Future _init(bool isBackground, {String via = ''}) async { ServiceLocator.instance .init(preferences, NetworkClient.instance.enteDio, packageInfo); - if (!isBackground) { - VideoPlayerMediaKit.ensureInitialized(iOS: true, android: true); - } - _logger.info("UserService init $tlog"); await UserService.instance.init(); _logger.info("UserService init done $tlog"); diff --git a/mobile/lib/ui/viewer/file/preview_video_widget.dart b/mobile/lib/ui/viewer/file/preview_video_widget.dart index be2c7034a8..e6ab5020c7 100644 --- a/mobile/lib/ui/viewer/file/preview_video_widget.dart +++ b/mobile/lib/ui/viewer/file/preview_video_widget.dart @@ -1,266 +1,266 @@ -import 'dart:async'; -import "dart:io"; +// import 'dart:async'; +// import "dart:io"; -import 'package:chewie/chewie.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import "package:fluttertoast/fluttertoast.dart"; -import "package:logging/logging.dart"; -import 'package:photos/core/constants.dart'; -import "package:photos/core/event_bus.dart"; -import "package:photos/events/guest_view_event.dart"; -import 'package:photos/models/file/file.dart'; -import "package:photos/service_locator.dart"; -import "package:photos/services/filedata/filedata_service.dart"; -import "package:photos/services/preview_video_store.dart"; -import "package:photos/ui/actions/file/file_actions.dart"; -import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -import "package:photos/ui/viewer/file/video_control.dart"; -import "package:photos/utils/data_util.dart"; -// import 'package:photos/ui/viewer/file/video_controls.dart'; -import "package:photos/utils/dialog_util.dart"; -import 'package:photos/utils/file_util.dart'; -import 'package:photos/utils/toast_util.dart'; -import "package:photos/utils/wakelock_util.dart"; -import 'package:video_player/video_player.dart'; -import 'package:visibility_detector/visibility_detector.dart'; +// import 'package:chewie/chewie.dart'; +// import 'package:flutter/cupertino.dart'; +// import 'package:flutter/material.dart'; +// import "package:fluttertoast/fluttertoast.dart"; +// import "package:logging/logging.dart"; +// import 'package:photos/core/constants.dart'; +// import "package:photos/core/event_bus.dart"; +// import "package:photos/events/guest_view_event.dart"; +// import 'package:photos/models/file/file.dart'; +// import "package:photos/service_locator.dart"; +// import "package:photos/services/filedata/filedata_service.dart"; +// import "package:photos/services/preview_video_store.dart"; +// import "package:photos/ui/actions/file/file_actions.dart"; +// import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; +// import "package:photos/ui/viewer/file/video_control.dart"; +// import "package:photos/utils/data_util.dart"; +// // import 'package:photos/ui/viewer/file/video_controls.dart'; +// import "package:photos/utils/dialog_util.dart"; +// import 'package:photos/utils/file_util.dart'; +// import 'package:photos/utils/toast_util.dart'; +// import "package:photos/utils/wakelock_util.dart"; +// import 'package:video_player/video_player.dart'; +// import 'package:visibility_detector/visibility_detector.dart'; -class PreviewVideoWidget extends StatefulWidget { - final EnteFile file; - final bool? autoPlay; - final String? tagPrefix; - final Function(bool)? playbackCallback; - final void Function()? onStreamChange; +// class PreviewVideoWidget extends StatefulWidget { +// final EnteFile file; +// final bool? autoPlay; +// final String? tagPrefix; +// final Function(bool)? playbackCallback; +// final void Function()? onStreamChange; - const PreviewVideoWidget( - this.file, { - this.autoPlay = true, - this.tagPrefix, - this.playbackCallback, - this.onStreamChange, - super.key, - }); +// const PreviewVideoWidget( +// this.file, { +// this.autoPlay = true, +// this.tagPrefix, +// this.playbackCallback, +// this.onStreamChange, +// super.key, +// }); - @override - State createState() => _PreviewVideoWidgetState(); -} +// @override +// State createState() => _PreviewVideoWidgetState(); +// } -class _PreviewVideoWidgetState extends State { - final _logger = Logger("PreviewVideoWidget"); - VideoPlayerController? _videoPlayerController; - ChewieController? _chewieController; - final _progressNotifier = ValueNotifier(null); - bool _isPlaying = false; - final EnteWakeLock _wakeLock = EnteWakeLock(); - bool _isFileSwipeLocked = false; - late final StreamSubscription _fileSwipeLockEventSubscription; - File? previewFile; +// class _PreviewVideoWidgetState extends State { +// final _logger = Logger("PreviewVideoWidget"); +// VideoPlayerController? _videoPlayerController; +// ChewieController? _chewieController; +// final _progressNotifier = ValueNotifier(null); +// bool _isPlaying = false; +// final EnteWakeLock _wakeLock = EnteWakeLock(); +// bool _isFileSwipeLocked = false; +// late final StreamSubscription _fileSwipeLockEventSubscription; +// File? previewFile; - @override - void initState() { - super.initState(); +// @override +// void initState() { +// super.initState(); - _checkForPreview(); - _fileSwipeLockEventSubscription = - Bus.instance.on().listen((event) { - setState(() { - _isFileSwipeLocked = event.swipeLocked; - }); - }); - } +// _checkForPreview(); +// _fileSwipeLockEventSubscription = +// Bus.instance.on().listen((event) { +// setState(() { +// _isFileSwipeLocked = event.swipeLocked; +// }); +// }); +// } - @override - void dispose() { - _fileSwipeLockEventSubscription.cancel(); - removeCallBack(widget.file); - _videoPlayerController?.dispose(); - _chewieController?.dispose(); - _progressNotifier.dispose(); - _wakeLock.dispose(); - super.dispose(); - } +// @override +// void dispose() { +// _fileSwipeLockEventSubscription.cancel(); +// removeCallBack(widget.file); +// _videoPlayerController?.dispose(); +// _chewieController?.dispose(); +// _progressNotifier.dispose(); +// _wakeLock.dispose(); +// super.dispose(); +// } - Future _checkForPreview() async { - final data = await PreviewVideoStore.instance - .getPlaylist(widget.file) - .onError((error, stackTrace) { - if (!mounted) return; - _logger.warning("Failed to download preview video", error, stackTrace); - Fluttertoast.showToast(msg: "Failed to download preview!"); - return null; - }); - if (!mounted) return; - if (data != null) { - if (flagService.internalUser) { - final d = - FileDataService.instance.previewIds?[widget.file.uploadedFileID!]; - if (d != null && widget.file.fileSize != null) { - // show toast with human readable size - final size = formatBytes(widget.file.fileSize!); - showToast( - context, - "Preview OG Size ($size), previewSize: ${formatBytes(d.objectSize)}", - ); - } else { - showShortToast(context, "Playing preview"); - } - } - previewFile = data.preview; - _setVideoPlayerController(); - } - } +// Future _checkForPreview() async { +// final data = await PreviewVideoStore.instance +// .getPlaylist(widget.file) +// .onError((error, stackTrace) { +// if (!mounted) return; +// _logger.warning("Failed to download preview video", error, stackTrace); +// Fluttertoast.showToast(msg: "Failed to download preview!"); +// return null; +// }); +// if (!mounted) return; +// if (data != null) { +// if (flagService.internalUser) { +// final d = +// FileDataService.instance.previewIds?[widget.file.uploadedFileID!]; +// if (d != null && widget.file.fileSize != null) { +// // show toast with human readable size +// final size = formatBytes(widget.file.fileSize!); +// showToast( +// context, +// "Preview OG Size ($size), previewSize: ${formatBytes(d.objectSize)}", +// ); +// } else { +// showShortToast(context, "Playing preview"); +// } +// } +// previewFile = data.preview; +// _setVideoPlayerController(); +// } +// } - void _setVideoPlayerController() { - if (!mounted) { - // Note: Do not initiale video player if widget is not mounted. - // On Android, if multiple instance of ExoPlayer is created, it will start - // resulting in playback errors for videos. See https://github.com/google/ExoPlayer/issues/6168 - return; - } - VideoPlayerController videoPlayerController; - videoPlayerController = VideoPlayerController.file(previewFile!); +// void _setVideoPlayerController() { +// if (!mounted) { +// // Note: Do not initiale video player if widget is not mounted. +// // On Android, if multiple instance of ExoPlayer is created, it will start +// // resulting in playback errors for videos. See https://github.com/google/ExoPlayer/issues/6168 +// return; +// } +// VideoPlayerController videoPlayerController; +// videoPlayerController = VideoPlayerController.file(previewFile!); - debugPrint("videoPlayerController: $videoPlayerController"); - _videoPlayerController = videoPlayerController - ..initialize().whenComplete(() { - if (mounted) { - setState(() {}); - } - }).onError( - (error, stackTrace) { - if (mounted && flagService.internalUser) { - if (error is Exception) { - showErrorDialogForException( - context: context, - exception: error, - message: "Failed to play video\n ${error.toString()}", - ); - } else { - showToast(context, "Failed to play video"); - } - } - }, - ); - } +// debugPrint("videoPlayerController: $videoPlayerController"); +// _videoPlayerController = videoPlayerController +// ..initialize().whenComplete(() { +// if (mounted) { +// setState(() {}); +// } +// }).onError( +// (error, stackTrace) { +// if (mounted && flagService.internalUser) { +// if (error is Exception) { +// showErrorDialogForException( +// context: context, +// exception: error, +// message: "Failed to play video\n ${error.toString()}", +// ); +// } else { +// showToast(context, "Failed to play video"); +// } +// } +// }, +// ); +// } - @override - Widget build(BuildContext context) { - final content = _videoPlayerController != null && - _videoPlayerController!.value.isInitialized - ? _getVideoPlayer() - : _getLoadingWidget(); - final contentWithDetector = GestureDetector( - onVerticalDragUpdate: _isFileSwipeLocked - ? null - : (d) => { - if (d.delta.dy > dragSensitivity) - { - Navigator.of(context).pop(), - } - else if (d.delta.dy < (dragSensitivity * -1)) - { - showDetailsSheet(context, widget.file), - }, - }, - child: content, - ); - return VisibilityDetector( - key: Key(widget.file.tag), - onVisibilityChanged: (info) { - if (info.visibleFraction < 1) { - if (mounted && _chewieController != null) { - _chewieController!.pause(); - } - } - }, - child: Hero( - tag: widget.tagPrefix! + widget.file.tag, - child: contentWithDetector, - ), - ); - } +// @override +// Widget build(BuildContext context) { +// final content = _videoPlayerController != null && +// _videoPlayerController!.value.isInitialized +// ? _getVideoPlayer() +// : _getLoadingWidget(); +// final contentWithDetector = GestureDetector( +// onVerticalDragUpdate: _isFileSwipeLocked +// ? null +// : (d) => { +// if (d.delta.dy > dragSensitivity) +// { +// Navigator.of(context).pop(), +// } +// else if (d.delta.dy < (dragSensitivity * -1)) +// { +// showDetailsSheet(context, widget.file), +// }, +// }, +// child: content, +// ); +// return VisibilityDetector( +// key: Key(widget.file.tag), +// onVisibilityChanged: (info) { +// if (info.visibleFraction < 1) { +// if (mounted && _chewieController != null) { +// _chewieController!.pause(); +// } +// } +// }, +// child: Hero( +// tag: widget.tagPrefix! + widget.file.tag, +// child: contentWithDetector, +// ), +// ); +// } - Widget _getLoadingWidget() { - return Stack( - children: [ - _getThumbnail(), - Container( - color: Colors.black12, - constraints: const BoxConstraints.expand(), - ), - Center( - child: SizedBox.fromSize( - size: const Size.square(20), - child: ValueListenableBuilder( - valueListenable: _progressNotifier, - builder: (BuildContext context, double? progress, _) { - return progress == null || progress == 1 - ? const CupertinoActivityIndicator( - color: Colors.white, - ) - : CircularProgressIndicator( - backgroundColor: Colors.black, - value: progress, - valueColor: const AlwaysStoppedAnimation( - Color.fromRGBO(45, 194, 98, 1.0), - ), - ); - }, - ), - ), - ), - ], - ); - } +// Widget _getLoadingWidget() { +// return Stack( +// children: [ +// _getThumbnail(), +// Container( +// color: Colors.black12, +// constraints: const BoxConstraints.expand(), +// ), +// Center( +// child: SizedBox.fromSize( +// size: const Size.square(20), +// child: ValueListenableBuilder( +// valueListenable: _progressNotifier, +// builder: (BuildContext context, double? progress, _) { +// return progress == null || progress == 1 +// ? const CupertinoActivityIndicator( +// color: Colors.white, +// ) +// : CircularProgressIndicator( +// backgroundColor: Colors.black, +// value: progress, +// valueColor: const AlwaysStoppedAnimation( +// Color.fromRGBO(45, 194, 98, 1.0), +// ), +// ); +// }, +// ), +// ), +// ), +// ], +// ); +// } - Widget _getThumbnail() { - return Container( - color: Colors.black, - constraints: const BoxConstraints.expand(), - child: ThumbnailWidget( - widget.file, - fit: BoxFit.contain, - ), - ); - } +// Widget _getThumbnail() { +// return Container( +// color: Colors.black, +// constraints: const BoxConstraints.expand(), +// child: ThumbnailWidget( +// widget.file, +// fit: BoxFit.contain, +// ), +// ); +// } - Future _keepScreenAliveOnPlaying(bool isPlaying) async { - if (isPlaying) { - _wakeLock.enable(); - } - if (!isPlaying) { - _wakeLock.disable(); - } - } +// Future _keepScreenAliveOnPlaying(bool isPlaying) async { +// if (isPlaying) { +// _wakeLock.enable(); +// } +// if (!isPlaying) { +// _wakeLock.disable(); +// } +// } - Widget _getVideoPlayer() { - _videoPlayerController!.addListener(() { - if (_isPlaying != _videoPlayerController!.value.isPlaying) { - _isPlaying = _videoPlayerController!.value.isPlaying; - if (widget.playbackCallback != null) { - widget.playbackCallback!(_isPlaying); - } - unawaited(_keepScreenAliveOnPlaying(_isPlaying)); - } - }); - _chewieController = ChewieController( - progressIndicatorDelay: const Duration(milliseconds: 200), - videoPlayerController: _videoPlayerController!, - aspectRatio: _videoPlayerController!.value.aspectRatio, - autoPlay: widget.autoPlay!, - autoInitialize: true, - looping: true, - allowMuting: true, - allowFullScreen: false, - customControls: VideoControls( - file: widget.file, - onStreamChange: widget.onStreamChange, - playbackCallback: widget.playbackCallback, - ), - ); - return Container( - color: Colors.black, - child: Chewie(controller: _chewieController!), - ); - } -} +// Widget _getVideoPlayer() { +// _videoPlayerController!.addListener(() { +// if (_isPlaying != _videoPlayerController!.value.isPlaying) { +// _isPlaying = _videoPlayerController!.value.isPlaying; +// if (widget.playbackCallback != null) { +// widget.playbackCallback!(_isPlaying); +// } +// unawaited(_keepScreenAliveOnPlaying(_isPlaying)); +// } +// }); +// _chewieController = ChewieController( +// progressIndicatorDelay: const Duration(milliseconds: 200), +// videoPlayerController: _videoPlayerController!, +// aspectRatio: _videoPlayerController!.value.aspectRatio, +// autoPlay: widget.autoPlay!, +// autoInitialize: true, +// looping: true, +// allowMuting: true, +// allowFullScreen: false, +// customControls: VideoControls( +// file: widget.file, +// onStreamChange: widget.onStreamChange, +// playbackCallback: widget.playbackCallback, +// ), +// ); +// return Container( +// color: Colors.black, +// child: Chewie(controller: _chewieController!), +// ); +// } +// } diff --git a/mobile/lib/ui/viewer/file/video_control.dart b/mobile/lib/ui/viewer/file/video_control.dart index 9d05b502fe..b6cfca4ba4 100644 --- a/mobile/lib/ui/viewer/file/video_control.dart +++ b/mobile/lib/ui/viewer/file/video_control.dart @@ -1,449 +1,449 @@ -// ignore_for_file: implementation_imports +// // ignore_for_file: implementation_imports -import 'dart:async'; +// import 'dart:async'; -import "package:chewie/chewie.dart"; -import "package:chewie/src/notifiers/index.dart"; -import 'package:flutter/material.dart'; -import "package:photos/models/file/file.dart"; -import "package:photos/theme/colors.dart"; -import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/viewer/file/preview_status_widget.dart"; -import "package:photos/ui/viewer/file/video_control/custom_progress_bar.dart"; -import "package:photos/utils/seconds_to_duration.dart"; -import 'package:provider/provider.dart'; -import 'package:video_player/video_player.dart'; +// import "package:chewie/chewie.dart"; +// import "package:chewie/src/notifiers/index.dart"; +// import 'package:flutter/material.dart'; +// import "package:photos/models/file/file.dart"; +// import "package:photos/theme/colors.dart"; +// import "package:photos/theme/ente_theme.dart"; +// import "package:photos/ui/common/loading_widget.dart"; +// import "package:photos/ui/viewer/file/preview_status_widget.dart"; +// import "package:photos/ui/viewer/file/video_control/custom_progress_bar.dart"; +// import "package:photos/utils/seconds_to_duration.dart"; +// import 'package:provider/provider.dart'; +// import 'package:video_player/video_player.dart'; -class VideoControls extends StatefulWidget { - const VideoControls({ - super.key, - required this.file, - required this.onStreamChange, - required this.playbackCallback, - }); - final EnteFile file; - final void Function()? onStreamChange; - final void Function(bool)? playbackCallback; +// class VideoControls extends StatefulWidget { +// const VideoControls({ +// super.key, +// required this.file, +// required this.onStreamChange, +// required this.playbackCallback, +// }); +// final EnteFile file; +// final void Function()? onStreamChange; +// final void Function(bool)? playbackCallback; - @override - State createState() { - return _VideoControlsState(); - } -} +// @override +// State createState() { +// return _VideoControlsState(); +// } +// } -class _VideoControlsState extends State - with SingleTickerProviderStateMixin { - late PlayerNotifier notifier; - late VideoPlayerValue _latestValue; - Timer? _hideTimer; - Timer? _initTimer; - Timer? _showAfterExpandCollapseTimer; - bool _dragging = false; - bool _displayTapped = false; - Timer? _bufferingDisplayTimer; - bool _displayBufferingIndicator = false; +// class _VideoControlsState extends State +// with SingleTickerProviderStateMixin { +// late PlayerNotifier notifier; +// late VideoPlayerValue _latestValue; +// Timer? _hideTimer; +// Timer? _initTimer; +// Timer? _showAfterExpandCollapseTimer; +// bool _dragging = false; +// bool _displayTapped = false; +// Timer? _bufferingDisplayTimer; +// bool _displayBufferingIndicator = false; - final barHeight = 48.0 * 1.5; - final marginSize = 5.0; +// final barHeight = 48.0 * 1.5; +// final marginSize = 5.0; - late VideoPlayerController controller; - ChewieController? _chewieController; +// late VideoPlayerController controller; +// ChewieController? _chewieController; - // We know that _chewieController is set in didChangeDependencies - ChewieController get chewieController => _chewieController!; +// // We know that _chewieController is set in didChangeDependencies +// ChewieController get chewieController => _chewieController!; - @override - void initState() { - super.initState(); - notifier = Provider.of(context, listen: false); - } +// @override +// void initState() { +// super.initState(); +// notifier = Provider.of(context, listen: false); +// } - @override - Widget build(BuildContext context) { - if (_latestValue.hasError) { - return chewieController.errorBuilder?.call( - context, - chewieController.videoPlayerController.value.errorDescription!, - ) ?? - Center( - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.onSurface, - size: 42, - ), - ); - } +// @override +// Widget build(BuildContext context) { +// if (_latestValue.hasError) { +// return chewieController.errorBuilder?.call( +// context, +// chewieController.videoPlayerController.value.errorDescription!, +// ) ?? +// Center( +// child: Icon( +// Icons.error, +// color: Theme.of(context).colorScheme.onSurface, +// size: 42, +// ), +// ); +// } - return MouseRegion( - onHover: (_) { - _cancelAndRestartTimer(); - }, - child: GestureDetector( - onTap: () => _cancelAndRestartTimer(), - child: AbsorbPointer( - absorbing: notifier.hideStuff, - child: Stack( - children: [ - if (_displayBufferingIndicator) - _chewieController?.bufferingBuilder?.call(context) ?? - const Center( - child: EnteLoadingWidget( - size: 32, - color: fillBaseDark, - padding: 0, - ), - ) - else - _buildHitArea(), - SafeArea( - top: false, - left: false, - right: false, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - PreviewStatusWidget( - showControls: !notifier.hideStuff, - file: widget.file, - isPreviewPlayer: true, - onStreamChange: widget.onStreamChange, - ), - if (!chewieController.isLive) _buildBottomBar(context), - ], - ), - ), - ], - ), - ), - ), - ); - } +// return MouseRegion( +// onHover: (_) { +// _cancelAndRestartTimer(); +// }, +// child: GestureDetector( +// onTap: () => _cancelAndRestartTimer(), +// child: AbsorbPointer( +// absorbing: notifier.hideStuff, +// child: Stack( +// children: [ +// if (_displayBufferingIndicator) +// _chewieController?.bufferingBuilder?.call(context) ?? +// const Center( +// child: EnteLoadingWidget( +// size: 32, +// color: fillBaseDark, +// padding: 0, +// ), +// ) +// else +// _buildHitArea(), +// SafeArea( +// top: false, +// left: false, +// right: false, +// child: Column( +// mainAxisAlignment: MainAxisAlignment.end, +// children: [ +// PreviewStatusWidget( +// showControls: !notifier.hideStuff, +// file: widget.file, +// isPreviewPlayer: true, +// onStreamChange: widget.onStreamChange, +// ), +// if (!chewieController.isLive) _buildBottomBar(context), +// ], +// ), +// ), +// ], +// ), +// ), +// ), +// ); +// } - @override - void dispose() { - _dispose(); - super.dispose(); - } +// @override +// void dispose() { +// _dispose(); +// super.dispose(); +// } - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - _initTimer?.cancel(); - _showAfterExpandCollapseTimer?.cancel(); - } +// void _dispose() { +// controller.removeListener(_updateState); +// _hideTimer?.cancel(); +// _initTimer?.cancel(); +// _showAfterExpandCollapseTimer?.cancel(); +// } - @override - void didChangeDependencies() { - final oldController = _chewieController; - _chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; +// @override +// void didChangeDependencies() { +// final oldController = _chewieController; +// _chewieController = ChewieController.of(context); +// controller = chewieController.videoPlayerController; - if (oldController != chewieController) { - _dispose(); - _initialize(); - } +// if (oldController != chewieController) { +// _dispose(); +// _initialize(); +// } - super.didChangeDependencies(); - } +// super.didChangeDependencies(); +// } - AnimatedOpacity _buildBottomBar( - BuildContext context, - ) { - return AnimatedOpacity( - opacity: notifier.hideStuff ? 0.0 : 1.0, - duration: const Duration(milliseconds: 300), - child: Container( - height: 40, - margin: const EdgeInsets.only(bottom: 60, left: 8, right: 8), - child: Container( - padding: const EdgeInsets.fromLTRB( - 16, - 4, - 16, - 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: Row( - children: [ - Text( - secondsToDuration(_latestValue.position.inSeconds), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildProgressBar(), - ), - const SizedBox(width: 16), - Text( - secondsToDuration(_latestValue.duration.inSeconds), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - ], - ), - ), - ), - ); - } +// AnimatedOpacity _buildBottomBar( +// BuildContext context, +// ) { +// return AnimatedOpacity( +// opacity: notifier.hideStuff ? 0.0 : 1.0, +// duration: const Duration(milliseconds: 300), +// child: Container( +// height: 40, +// margin: const EdgeInsets.only(bottom: 60, left: 8, right: 8), +// child: Container( +// padding: const EdgeInsets.fromLTRB( +// 16, +// 4, +// 16, +// 4, +// ), +// decoration: BoxDecoration( +// color: Colors.black.withOpacity(0.3), +// borderRadius: const BorderRadius.all( +// Radius.circular(8), +// ), +// border: Border.all( +// color: strokeFaintDark, +// width: 1, +// ), +// ), +// child: Row( +// children: [ +// Text( +// secondsToDuration(_latestValue.position.inSeconds), +// style: getEnteTextTheme( +// context, +// ).mini.copyWith( +// color: textBaseDark, +// ), +// ), +// const SizedBox(width: 16), +// Expanded( +// child: _buildProgressBar(), +// ), +// const SizedBox(width: 16), +// Text( +// secondsToDuration(_latestValue.duration.inSeconds), +// style: getEnteTextTheme( +// context, +// ).mini.copyWith( +// color: textBaseDark, +// ), +// ), +// ], +// ), +// ), +// ), +// ); +// } - Widget _buildHitArea() { - final bool isFinished = (_latestValue.position >= _latestValue.duration) && - _latestValue.duration.inSeconds > 0; - final bool showPlayButton = true && !_dragging && !notifier.hideStuff; +// Widget _buildHitArea() { +// final bool isFinished = (_latestValue.position >= _latestValue.duration) && +// _latestValue.duration.inSeconds > 0; +// final bool showPlayButton = true && !_dragging && !notifier.hideStuff; - return GestureDetector( - onTap: () { - if (_latestValue.isPlaying) { - if (_displayTapped) { - setState(() { - notifier.hideStuff = true; - }); - } else { - _cancelAndRestartTimer(); - } - } else { - setState(() { - notifier.hideStuff = true; - }); - } - widget.playbackCallback?.call(notifier.hideStuff); - }, - child: Container( - alignment: Alignment.center, - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: EdgeInsets.symmetric( - horizontal: marginSize, - ), - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: isFinished, - isPlaying: controller.value.isPlaying, - show: showPlayButton, - onPressed: _playPause, - ), - ), - ], - ), - ), - ); - } +// return GestureDetector( +// onTap: () { +// if (_latestValue.isPlaying) { +// if (_displayTapped) { +// setState(() { +// notifier.hideStuff = true; +// }); +// } else { +// _cancelAndRestartTimer(); +// } +// } else { +// setState(() { +// notifier.hideStuff = true; +// }); +// } +// widget.playbackCallback?.call(notifier.hideStuff); +// }, +// child: Container( +// alignment: Alignment.center, +// color: Colors.transparent, +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Container( +// margin: EdgeInsets.symmetric( +// horizontal: marginSize, +// ), +// child: CenterPlayButton( +// backgroundColor: Colors.black54, +// iconColor: Colors.white, +// isFinished: isFinished, +// isPlaying: controller.value.isPlaying, +// show: showPlayButton, +// onPressed: _playPause, +// ), +// ), +// ], +// ), +// ), +// ); +// } - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - _startHideTimer(); +// void _cancelAndRestartTimer() { +// _hideTimer?.cancel(); +// _startHideTimer(); - setState(() { - notifier.hideStuff = false; - _displayTapped = true; - }); - widget.playbackCallback?.call(notifier.hideStuff); - } +// setState(() { +// notifier.hideStuff = false; +// _displayTapped = true; +// }); +// widget.playbackCallback?.call(notifier.hideStuff); +// } - Future _initialize() async { - controller.addListener(_updateState); +// Future _initialize() async { +// controller.addListener(_updateState); - _updateState(); +// _updateState(); - if (controller.value.isPlaying || chewieController.autoPlay) { - _startHideTimer(); - } +// if (controller.value.isPlaying || chewieController.autoPlay) { +// _startHideTimer(); +// } - if (chewieController.showControlsOnInitialize) { - _initTimer = Timer(const Duration(milliseconds: 200), () { - setState(() { - notifier.hideStuff = false; - }); - widget.playbackCallback?.call(notifier.hideStuff); - }); - } - } +// if (chewieController.showControlsOnInitialize) { +// _initTimer = Timer(const Duration(milliseconds: 200), () { +// setState(() { +// notifier.hideStuff = false; +// }); +// widget.playbackCallback?.call(notifier.hideStuff); +// }); +// } +// } - void _playPause() { - final bool isFinished = (_latestValue.position >= _latestValue.duration) && - _latestValue.duration.inSeconds > 0; +// void _playPause() { +// final bool isFinished = (_latestValue.position >= _latestValue.duration) && +// _latestValue.duration.inSeconds > 0; - setState(() { - if (controller.value.isPlaying) { - notifier.hideStuff = false; - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); +// setState(() { +// if (controller.value.isPlaying) { +// notifier.hideStuff = false; +// _hideTimer?.cancel(); +// controller.pause(); +// } else { +// _cancelAndRestartTimer(); - if (!controller.value.isInitialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(Duration.zero); - } - controller.play(); - } - } - widget.playbackCallback?.call(notifier.hideStuff); - }); - } +// if (!controller.value.isInitialized) { +// controller.initialize().then((_) { +// controller.play(); +// }); +// } else { +// if (isFinished) { +// controller.seekTo(Duration.zero); +// } +// controller.play(); +// } +// } +// widget.playbackCallback?.call(notifier.hideStuff); +// }); +// } - void _startHideTimer() { - final hideControlsTimer = chewieController.hideControlsTimer.isNegative - ? ChewieController.defaultHideControlsTimer - : chewieController.hideControlsTimer; - _hideTimer = Timer(hideControlsTimer, () { - setState(() { - notifier.hideStuff = true; - }); - widget.playbackCallback?.call(notifier.hideStuff); - }); - } +// void _startHideTimer() { +// final hideControlsTimer = chewieController.hideControlsTimer.isNegative +// ? ChewieController.defaultHideControlsTimer +// : chewieController.hideControlsTimer; +// _hideTimer = Timer(hideControlsTimer, () { +// setState(() { +// notifier.hideStuff = true; +// }); +// widget.playbackCallback?.call(notifier.hideStuff); +// }); +// } - void _bufferingTimerTimeout() { - _displayBufferingIndicator = true; - if (mounted) { - setState(() {}); - } - } +// void _bufferingTimerTimeout() { +// _displayBufferingIndicator = true; +// if (mounted) { +// setState(() {}); +// } +// } - void _updateState() { - if (!mounted) return; +// void _updateState() { +// if (!mounted) return; - // display the progress bar indicator only after the buffering delay if it has been set - if (chewieController.progressIndicatorDelay != null) { - if (controller.value.isBuffering) { - _bufferingDisplayTimer ??= Timer( - chewieController.progressIndicatorDelay!, - _bufferingTimerTimeout, - ); - } else { - _bufferingDisplayTimer?.cancel(); - _bufferingDisplayTimer = null; - _displayBufferingIndicator = false; - } - } else { - _displayBufferingIndicator = controller.value.isBuffering; - } +// // display the progress bar indicator only after the buffering delay if it has been set +// if (chewieController.progressIndicatorDelay != null) { +// if (controller.value.isBuffering) { +// _bufferingDisplayTimer ??= Timer( +// chewieController.progressIndicatorDelay!, +// _bufferingTimerTimeout, +// ); +// } else { +// _bufferingDisplayTimer?.cancel(); +// _bufferingDisplayTimer = null; +// _displayBufferingIndicator = false; +// } +// } else { +// _displayBufferingIndicator = controller.value.isBuffering; +// } - setState(() { - _latestValue = controller.value; - }); - } +// setState(() { +// _latestValue = controller.value; +// }); +// } - Widget _buildProgressBar() { - final colorScheme = getEnteColorScheme(context); - return Expanded( - child: CustomProgressBar( - controller, - onDragStart: () { - setState(() { - _dragging = true; - }); +// Widget _buildProgressBar() { +// final colorScheme = getEnteColorScheme(context); +// return Expanded( +// child: CustomProgressBar( +// controller, +// onDragStart: () { +// setState(() { +// _dragging = true; +// }); - _hideTimer?.cancel(); - }, - onDragUpdate: () { - _hideTimer?.cancel(); - }, - onDragEnd: () { - setState(() { - _dragging = false; - }); +// _hideTimer?.cancel(); +// }, +// onDragUpdate: () { +// _hideTimer?.cancel(); +// }, +// onDragEnd: () { +// setState(() { +// _dragging = false; +// }); - _startHideTimer(); - }, - colors: ChewieProgressColors( - playedColor: colorScheme.primary300, - handleColor: backgroundElevatedLight, - bufferedColor: backgroundElevatedLight.withOpacity(0.5), - backgroundColor: fillMutedDark, - ), - draggableProgressBar: chewieController.draggableProgressBar, - ), - ); - } -} +// _startHideTimer(); +// }, +// colors: ChewieProgressColors( +// playedColor: colorScheme.primary300, +// handleColor: backgroundElevatedLight, +// bufferedColor: backgroundElevatedLight.withOpacity(0.5), +// backgroundColor: fillMutedDark, +// ), +// draggableProgressBar: chewieController.draggableProgressBar, +// ), +// ); +// } +// } -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); +// class CenterPlayButton extends StatelessWidget { +// const CenterPlayButton({ +// super.key, +// required this.backgroundColor, +// this.iconColor, +// required this.show, +// required this.isPlaying, +// required this.isFinished, +// this.onPressed, +// }); - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; +// final Color backgroundColor; +// final Color? iconColor; +// final bool show; +// final bool isPlaying; +// final bool isFinished; +// final VoidCallback? onPressed; - @override - Widget build(BuildContext context) { - return AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: Container( - width: 54, - height: 54, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onPressed, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - switchInCurve: Curves.easeInOutQuart, - switchOutCurve: Curves.easeInOutQuart, - child: isPlaying - ? const Icon( - Icons.pause, - size: 32, - key: ValueKey("pause"), - color: Colors.white, - ) - : const Icon( - Icons.play_arrow, - size: 36, - key: ValueKey("play"), - color: Colors.white, - ), - ), - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return AnimatedOpacity( +// opacity: show ? 1.0 : 0.0, +// duration: const Duration(milliseconds: 300), +// child: Container( +// width: 54, +// height: 54, +// decoration: BoxDecoration( +// color: Colors.black.withOpacity(0.3), +// shape: BoxShape.circle, +// border: Border.all( +// color: strokeFaintDark, +// width: 1, +// ), +// ), +// child: GestureDetector( +// behavior: HitTestBehavior.opaque, +// onTap: onPressed, +// child: AnimatedSwitcher( +// duration: const Duration(milliseconds: 250), +// transitionBuilder: (Widget child, Animation animation) { +// return ScaleTransition(scale: animation, child: child); +// }, +// switchInCurve: Curves.easeInOutQuart, +// switchOutCurve: Curves.easeInOutQuart, +// child: isPlaying +// ? const Icon( +// Icons.pause, +// size: 32, +// key: ValueKey("pause"), +// color: Colors.white, +// ) +// : const Icon( +// Icons.play_arrow, +// size: 36, +// key: ValueKey("play"), +// color: Colors.white, +// ), +// ), +// ), +// ), +// ); +// } +// } diff --git a/mobile/lib/ui/viewer/file/video_widget.dart b/mobile/lib/ui/viewer/file/video_widget.dart index 237e4cdaca..e93c353617 100644 --- a/mobile/lib/ui/viewer/file/video_widget.dart +++ b/mobile/lib/ui/viewer/file/video_widget.dart @@ -7,8 +7,8 @@ import "package:photos/events/use_media_kit_for_video.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/filedata/filedata_service.dart"; import "package:photos/services/preview_video_store.dart"; -import "package:photos/ui/viewer/file/preview_video_widget.dart"; import "package:photos/ui/viewer/file/video_widget_media_kit_new.dart"; +import "package:photos/ui/viewer/file/video_widget_media_kit_preview.dart"; import "package:photos/ui/viewer/file/video_widget_native.dart"; class VideoWidget extends StatefulWidget { @@ -60,7 +60,7 @@ class _VideoWidgetState extends State { ?.containsKey(widget.file.uploadedFileID!) ?? false); if (isPreviewVideoPlayable && selectPreviewForPlay) { - return PreviewVideoWidget( + return VideoWidgetMediaKitPreview( widget.file, tagPrefix: widget.tagPrefix, playbackCallback: widget.playbackCallback, diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart new file mode 100644 index 0000000000..a5f2be3e8c --- /dev/null +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart @@ -0,0 +1,458 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:media_kit_video/media_kit_video.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/theme/colors.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/viewer/file/preview_status_widget.dart"; +import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/seconds_to_duration.dart"; + +class VideoWidget extends StatefulWidget { + final EnteFile file; + final VideoController controller; + final Function(bool)? playbackCallback; + final bool isFromMemories; + final void Function() onStreamChange; + + const VideoWidget( + this.file, + this.controller, + this.playbackCallback, { + super.key, + required this.isFromMemories, + // ignore: unused_element + required this.onStreamChange, + }); + + @override + State createState() => _VideoWidgetState(); +} + +class _VideoWidgetState extends State { + final showControlsNotifier = ValueNotifier(true); + static const verticalMargin = 72.0; + final _hideControlsDebouncer = Debouncer( + const Duration(milliseconds: 2000), + ); + final _isSeekingNotifier = ValueNotifier(false); + late final StreamSubscription _isPlayingStreamSubscription; + + @override + void initState() { + _isPlayingStreamSubscription = + widget.controller.player.stream.playing.listen((isPlaying) { + if (isPlaying && !_isSeekingNotifier.value) { + _hideControlsDebouncer.run(() async { + showControlsNotifier.value = false; + widget.playbackCallback?.call(true); + }); + } + }); + + _isSeekingNotifier.addListener(isSeekingListener); + super.initState(); + } + + @override + void dispose() { + showControlsNotifier.dispose(); + _isPlayingStreamSubscription.cancel(); + _hideControlsDebouncer.cancelDebounceTimer(); + _isSeekingNotifier.removeListener(isSeekingListener); + _isSeekingNotifier.dispose(); + super.dispose(); + } + + void isSeekingListener() { + if (_isSeekingNotifier.value) { + _hideControlsDebouncer.cancelDebounceTimer(); + } else { + if (widget.controller.player.state.playing) { + _hideControlsDebouncer.run(() async { + showControlsNotifier.value = false; + widget.playbackCallback?.call(false); + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Video( + controller: widget.controller, + controls: (state) { + return ValueListenableBuilder( + valueListenable: showControlsNotifier, + builder: (context, value, _) { + return AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: value ? 1 : 0, + curve: Curves.easeInOutQuad, + child: Stack( + alignment: Alignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + showControlsNotifier.value = !showControlsNotifier.value; + if (widget.playbackCallback != null) { + widget.playbackCallback!( + !showControlsNotifier.value, + ); + } + }, + child: Container( + constraints: const BoxConstraints.expand(), + ), + ), + IgnorePointer( + ignoring: !value, + child: PlayPauseButtonMediaKit(widget.controller), + ), + Positioned( + bottom: verticalMargin, + right: 0, + left: 0, + child: IgnorePointer( + ignoring: !value, + child: SafeArea( + top: false, + left: false, + right: false, + child: Padding( + padding: EdgeInsets.only( + bottom: widget.isFromMemories ? 32 : 0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.file.caption != null + ? Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 12, + 16, + 8, + ), + child: Text( + widget.file.caption!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: getEnteTextTheme(context) + .mini + .copyWith( + color: textBaseDark, + ), + textAlign: TextAlign.center, + ), + ) + : const SizedBox.shrink(), + PreviewStatusWidget( + showControls: value, + file: widget.file, + isPreviewPlayer: true, + onStreamChange: widget.onStreamChange, + ), + SeekBarAndDuration( + controller: widget.controller, + isSeekingNotifier: _isSeekingNotifier, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} + +class PlayPauseButtonMediaKit extends StatefulWidget { + final VideoController? controller; + const PlayPauseButtonMediaKit( + this.controller, { + super.key, + }); + + @override + State createState() => _PlayPauseButtonState(); +} + +class _PlayPauseButtonState extends State { + bool _isPlaying = true; + late final StreamSubscription? isPlayingStreamSubscription; + + @override + void initState() { + super.initState(); + + isPlayingStreamSubscription = + widget.controller?.player.stream.playing.listen((isPlaying) { + setState(() { + _isPlaying = isPlaying; + }); + }); + } + + @override + void dispose() { + isPlayingStreamSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (widget.controller?.player.state.playing ?? false) { + widget.controller?.player.pause(); + } else { + widget.controller?.player.play(); + } + }, + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + shape: BoxShape.circle, + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + switchInCurve: Curves.easeInOutQuart, + switchOutCurve: Curves.easeInOutQuart, + child: _isPlaying + ? const Icon( + Icons.pause, + size: 32, + key: ValueKey("pause"), + color: Colors.white, + ) + : const Icon( + Icons.play_arrow, + size: 36, + key: ValueKey("play"), + color: Colors.white, + ), + ), + ), + ); + } +} + +class SeekBarAndDuration extends StatelessWidget { + final VideoController? controller; + final ValueNotifier isSeekingNotifier; + + const SeekBarAndDuration({ + super.key, + required this.controller, + required this.isSeekingNotifier, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Container( + padding: const EdgeInsets.fromLTRB( + 16, + 4, + 16, + 4, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: Row( + children: [ + StreamBuilder( + stream: controller?.player.stream.position, + builder: (context, snapshot) { + if (snapshot.data == null) { + return Text( + "0:00", + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ); + } + return Text( + secondsToDuration(snapshot.data!.inSeconds), + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ); + }, + ), + Expanded( + child: SeekBar( + controller!, + isSeekingNotifier, + ), + ), + Text( + _secondsToDuration( + controller!.player.state.duration.inSeconds, + ), + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ), + ], + ), + ), + ); + } + + /// Returns the duration in the format "h:mm:ss" or "m:ss". + String _secondsToDuration(int totalSeconds) { + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final seconds = totalSeconds % 60; + + if (hours > 0) { + return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + } +} + +class SeekBar extends StatefulWidget { + final VideoController controller; + final ValueNotifier isSeekingNotifier; + const SeekBar( + this.controller, + this.isSeekingNotifier, { + super.key, + }); + + @override + State createState() => _SeekBarState(); +} + +class _SeekBarState extends State { + double _sliderValue = 0.0; + late final StreamSubscription _positionStreamSubscription; + final _debouncer = Debouncer( + const Duration(milliseconds: 300), + executionInterval: const Duration(milliseconds: 300), + ); + @override + void initState() { + super.initState(); + _positionStreamSubscription = + widget.controller.player.stream.position.listen((event) { + if (widget.isSeekingNotifier.value) return; + if (mounted) { + setState(() { + _sliderValue = event.inMilliseconds / + widget.controller.player.state.duration.inMilliseconds; + if (_sliderValue.isNaN) { + _sliderValue = 0.0; + } + }); + } + }); + } + + @override + void dispose() { + _positionStreamSubscription.cancel(); + _debouncer.cancelDebounceTimer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 1.0, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0), + activeTrackColor: colorScheme.primary300, + inactiveTrackColor: fillMutedDark, + thumbColor: backgroundElevatedLight, + overlayColor: fillMutedDark, + ), + child: Slider( + min: 0.0, + max: 1.0, + value: _sliderValue, + onChangeStart: (value) { + if (mounted) { + setState(() { + widget.isSeekingNotifier.value = true; + }); + } + }, + onChanged: (value) { + if (mounted) { + setState(() { + _sliderValue = value; + }); + } + + _debouncer.run(() async { + await widget.controller.player.seek( + Duration( + milliseconds: (value * + widget.controller.player.state.duration.inMilliseconds) + .round(), + ), + ); + }); + }, + divisions: 4500, + onChangeEnd: (value) async { + await widget.controller.player.seek( + Duration( + milliseconds: (value * + widget.controller.player.state.duration.inMilliseconds) + .round(), + ), + ); + if (mounted) { + setState(() { + widget.isSeekingNotifier.value = false; + }); + } + }, + allowedInteraction: SliderInteraction.tapAndSlide, + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart index fe5c5d13b4..13d45bd57a 100644 --- a/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_new.dart @@ -14,13 +14,12 @@ import "package:photos/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/files_service.dart"; import "package:photos/theme/colors.dart"; -import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/actions/file/file_actions.dart"; -import "package:photos/ui/viewer/file/preview_status_widget.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/viewer/file/video_widget_media_kit_common.dart" + as common; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_util.dart"; -import "package:photos/utils/seconds_to_duration.dart"; import "package:photos/utils/toast_util.dart"; class VideoWidgetMediaKitNew extends StatefulWidget { @@ -28,14 +27,14 @@ class VideoWidgetMediaKitNew extends StatefulWidget { final String? tagPrefix; final Function(bool)? playbackCallback; final bool isFromMemories; - final void Function()? onStreamChange; + final void Function() onStreamChange; const VideoWidgetMediaKitNew( this.file, { this.tagPrefix, this.playbackCallback, this.isFromMemories = false, - this.onStreamChange, + required this.onStreamChange, super.key, }); @@ -138,44 +137,20 @@ class _VideoWidgetMediaKitNewState extends State }, child: Center( child: controller != null - ? _VideoWidget( + ? common.VideoWidget( widget.file, controller!, widget.playbackCallback, isFromMemories: widget.isFromMemories, + onStreamChange: widget.onStreamChange, ) - // : Stack( - // children: [ - // _getThumbnail(), - // Container( - // color: Colors.black12, - // constraints: const BoxConstraints.expand(), - // ), - // Center( - // child: SizedBox.fromSize( - // size: const Size.square(20), - // child: ValueListenableBuilder( - // valueListenable: _progressNotifier, - // builder: (BuildContext context, double? progress, _) { - // return progress == null || progress == 1 - // ? const CupertinoActivityIndicator( - // color: Colors.white, - // ) - // : CircularProgressIndicator( - // backgroundColor: Colors.black, - // value: progress, - // valueColor: - // const AlwaysStoppedAnimation( - // Color.fromRGBO(45, 194, 98, 1.0), - // ), - // ); - // }, - // ), - // ), - // ), - // ], - // ), - : const SizedBox.shrink(), + : const Center( + child: EnteLoadingWidget( + size: 32, + color: fillBaseDark, + padding: 0, + ), + ), ), ); } @@ -230,452 +205,3 @@ class _VideoWidgetMediaKitNewState extends State } } } - -class _VideoWidget extends StatefulWidget { - final EnteFile file; - final VideoController controller; - final Function(bool)? playbackCallback; - final bool isFromMemories; - final void Function()? onStreamChange; - - const _VideoWidget( - this.file, - this.controller, - this.playbackCallback, { - required this.isFromMemories, - // ignore: unused_element - this.onStreamChange, - }); - - @override - State<_VideoWidget> createState() => __VideoWidgetState(); -} - -class __VideoWidgetState extends State<_VideoWidget> { - final showControlsNotifier = ValueNotifier(true); - static const verticalMargin = 72.0; - final _hideControlsDebouncer = Debouncer( - const Duration(milliseconds: 2000), - ); - final _isSeekingNotifier = ValueNotifier(false); - late final StreamSubscription _isPlayingStreamSubscription; - - @override - void initState() { - _isPlayingStreamSubscription = - widget.controller.player.stream.playing.listen((isPlaying) { - if (isPlaying && !_isSeekingNotifier.value) { - _hideControlsDebouncer.run(() async { - showControlsNotifier.value = false; - widget.playbackCallback?.call(true); - }); - } - }); - - _isSeekingNotifier.addListener(isSeekingListener); - super.initState(); - } - - @override - void dispose() { - showControlsNotifier.dispose(); - _isPlayingStreamSubscription.cancel(); - _hideControlsDebouncer.cancelDebounceTimer(); - _isSeekingNotifier.removeListener(isSeekingListener); - _isSeekingNotifier.dispose(); - super.dispose(); - } - - void isSeekingListener() { - if (_isSeekingNotifier.value) { - _hideControlsDebouncer.cancelDebounceTimer(); - } else { - if (widget.controller.player.state.playing) { - _hideControlsDebouncer.run(() async { - showControlsNotifier.value = false; - widget.playbackCallback?.call(false); - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Video( - controller: widget.controller, - controls: (state) { - return ValueListenableBuilder( - valueListenable: showControlsNotifier, - builder: (context, value, _) { - return AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: value ? 1 : 0, - curve: Curves.easeInOutQuad, - child: Stack( - alignment: Alignment.center, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - showControlsNotifier.value = !showControlsNotifier.value; - if (widget.playbackCallback != null) { - widget.playbackCallback!( - !showControlsNotifier.value, - ); - } - }, - child: Container( - constraints: const BoxConstraints.expand(), - ), - ), - IgnorePointer( - ignoring: !value, - child: PlayPauseButtonMediaKit(widget.controller), - ), - Positioned( - bottom: verticalMargin, - right: 0, - left: 0, - child: IgnorePointer( - ignoring: !value, - child: SafeArea( - top: false, - left: false, - right: false, - child: Padding( - padding: EdgeInsets.only( - bottom: widget.isFromMemories ? 32 : 0, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.file.caption != null - ? Padding( - padding: const EdgeInsets.fromLTRB( - 16, - 12, - 16, - 8, - ), - child: Text( - widget.file.caption!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: getEnteTextTheme(context) - .mini - .copyWith( - color: textBaseDark, - ), - textAlign: TextAlign.center, - ), - ) - : const SizedBox.shrink(), - ValueListenableBuilder( - valueListenable: showControlsNotifier, - builder: (context, value, _) { - return PreviewStatusWidget( - showControls: value, - file: widget.file, - onStreamChange: widget.onStreamChange, - ); - }, - ), - _SeekBarAndDuration( - controller: widget.controller, - isSeekingNotifier: _isSeekingNotifier, - ), - ], - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } -} - -class PlayPauseButtonMediaKit extends StatefulWidget { - final VideoController? controller; - const PlayPauseButtonMediaKit( - this.controller, { - super.key, - }); - - @override - State createState() => _PlayPauseButtonState(); -} - -class _PlayPauseButtonState extends State { - bool _isPlaying = true; - late final StreamSubscription? isPlayingStreamSubscription; - - @override - void initState() { - super.initState(); - - isPlayingStreamSubscription = - widget.controller?.player.stream.playing.listen((isPlaying) { - setState(() { - _isPlaying = isPlaying; - }); - }); - } - - @override - void dispose() { - isPlayingStreamSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (widget.controller?.player.state.playing ?? false) { - widget.controller?.player.pause(); - } else { - widget.controller?.player.play(); - } - }, - child: Container( - width: 54, - height: 54, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - switchInCurve: Curves.easeInOutQuart, - switchOutCurve: Curves.easeInOutQuart, - child: _isPlaying - ? const Icon( - Icons.pause, - size: 32, - key: ValueKey("pause"), - color: Colors.white, - ) - : const Icon( - Icons.play_arrow, - size: 36, - key: ValueKey("play"), - color: Colors.white, - ), - ), - ), - ); - } -} - -class _SeekBarAndDuration extends StatelessWidget { - final VideoController? controller; - final ValueNotifier isSeekingNotifier; - - const _SeekBarAndDuration({ - required this.controller, - required this.isSeekingNotifier, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: Container( - padding: const EdgeInsets.fromLTRB( - 16, - 4, - 16, - 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: Row( - children: [ - StreamBuilder( - stream: controller?.player.stream.position, - builder: (context, snapshot) { - if (snapshot.data == null) { - return Text( - "0:00", - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ); - } - return Text( - secondsToDuration(snapshot.data!.inSeconds), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ); - }, - ), - Expanded( - child: _SeekBar( - controller!, - isSeekingNotifier, - ), - ), - Text( - _secondsToDuration( - controller!.player.state.duration.inSeconds, - ), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - ], - ), - ), - ); - } - - /// Returns the duration in the format "h:mm:ss" or "m:ss". - String _secondsToDuration(int totalSeconds) { - final hours = totalSeconds ~/ 3600; - final minutes = (totalSeconds % 3600) ~/ 60; - final seconds = totalSeconds % 60; - - if (hours > 0) { - return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } else { - return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - } -} - -class _SeekBar extends StatefulWidget { - final VideoController controller; - final ValueNotifier isSeekingNotifier; - const _SeekBar( - this.controller, - this.isSeekingNotifier, - ); - - @override - State<_SeekBar> createState() => _SeekBarState(); -} - -class _SeekBarState extends State<_SeekBar> { - double _sliderValue = 0.0; - late final StreamSubscription _positionStreamSubscription; - final _debouncer = Debouncer( - const Duration(milliseconds: 300), - executionInterval: const Duration(milliseconds: 300), - ); - @override - void initState() { - super.initState(); - _positionStreamSubscription = - widget.controller.player.stream.position.listen((event) { - if (widget.isSeekingNotifier.value) return; - if (mounted) { - setState(() { - _sliderValue = event.inMilliseconds / - widget.controller.player.state.duration.inMilliseconds; - if (_sliderValue.isNaN) { - _sliderValue = 0.0; - } - }); - } - }); - } - - @override - void dispose() { - _positionStreamSubscription.cancel(); - _debouncer.cancelDebounceTimer(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); - return SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 1.0, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), - overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0), - activeTrackColor: colorScheme.primary300, - inactiveTrackColor: fillMutedDark, - thumbColor: backgroundElevatedLight, - overlayColor: fillMutedDark, - ), - child: Slider( - min: 0.0, - max: 1.0, - value: _sliderValue, - onChangeStart: (value) { - if (mounted) { - setState(() { - widget.isSeekingNotifier.value = true; - }); - } - }, - onChanged: (value) { - if (mounted) { - setState(() { - _sliderValue = value; - }); - } - - _debouncer.run(() async { - await widget.controller.player.seek( - Duration( - milliseconds: (value * - widget.controller.player.state.duration.inMilliseconds) - .round(), - ), - ); - }); - }, - divisions: 4500, - onChangeEnd: (value) async { - await widget.controller.player.seek( - Duration( - milliseconds: (value * - widget.controller.player.state.duration.inMilliseconds) - .round(), - ), - ); - if (mounted) { - setState(() { - widget.isSeekingNotifier.value = false; - }); - } - }, - allowedInteraction: SliderInteraction.tapAndSlide, - ), - ); - } -} diff --git a/mobile/lib/ui/viewer/file/video_widget_media_kit_preview.dart b/mobile/lib/ui/viewer/file/video_widget_media_kit_preview.dart new file mode 100644 index 0000000000..39ff6e491f --- /dev/null +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_preview.dart @@ -0,0 +1,172 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:fluttertoast/fluttertoast.dart"; +import "package:logging/logging.dart"; +import "package:media_kit/media_kit.dart"; +import "package:media_kit_video/media_kit_video.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/events/guest_view_event.dart"; +import "package:photos/events/pause_video_event.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/filedata/filedata_service.dart"; +import "package:photos/services/preview_video_store.dart"; +import "package:photos/theme/colors.dart"; +import "package:photos/ui/actions/file/file_actions.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/viewer/file/video_widget_media_kit_common.dart" + as common; +import "package:photos/utils/data_util.dart"; +import "package:photos/utils/file_util.dart"; +import "package:photos/utils/toast_util.dart"; + +class VideoWidgetMediaKitPreview extends StatefulWidget { + final EnteFile file; + final String? tagPrefix; + final Function(bool)? playbackCallback; + final bool isFromMemories; + final void Function() onStreamChange; + + const VideoWidgetMediaKitPreview( + this.file, { + this.tagPrefix, + this.playbackCallback, + this.isFromMemories = false, + required this.onStreamChange, + super.key, + }); + + @override + State createState() => + _VideoWidgetMediaKitPreviewState(); +} + +class _VideoWidgetMediaKitPreviewState extends State + with WidgetsBindingObserver { + final Logger _logger = Logger("VideoWidgetMediaKitNew"); + late final player = Player(); + VideoController? controller; + final _progressNotifier = ValueNotifier(null); + bool _isAppInFG = true; + late StreamSubscription pauseVideoSubscription; + bool isGuestView = false; + late final StreamSubscription _guestViewEventSubscription; + bool _isGuestView = false; + + @override + void initState() { + _logger.info( + 'initState for ${widget.file.generatedID} with tag ${widget.file.tag} and name ${widget.file.displayName}', + ); + super.initState(); + WidgetsBinding.instance.addObserver(this); + _checkForPreview(); + + pauseVideoSubscription = Bus.instance.on().listen((event) { + player.pause(); + }); + _guestViewEventSubscription = + Bus.instance.on().listen((event) { + setState(() { + _isGuestView = event.isGuestView; + }); + }); + } + + Future _checkForPreview() async { + widget.playbackCallback?.call(false); + final data = await PreviewVideoStore.instance + .getPlaylist(widget.file) + .onError((error, stackTrace) { + if (!mounted) return; + _logger.warning("Failed to download preview video", error, stackTrace); + Fluttertoast.showToast(msg: "Failed to download preview!"); + return null; + }); + if (!mounted) return; + if (data != null) { + if (flagService.internalUser) { + final d = + FileDataService.instance.previewIds?[widget.file.uploadedFileID!]; + if (d != null && widget.file.fileSize != null) { + // show toast with human readable size + final size = formatBytes(widget.file.fileSize!); + showToast( + context, + "[i] Preview OG Size ($size), previewSize: ${formatBytes(d.objectSize)}", + ); + } else { + showShortToast(context, "Playing preview"); + } + } + _setVideoController(data.preview.path); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _isAppInFG = true; + } else { + _isAppInFG = false; + } + } + + @override + void dispose() { + _guestViewEventSubscription.cancel(); + pauseVideoSubscription.cancel(); + removeCallBack(widget.file); + _progressNotifier.dispose(); + WidgetsBinding.instance.removeObserver(this); + player.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: _isGuestView + ? null + : (d) => { + if (d.delta.dy > dragSensitivity) + { + Navigator.of(context).pop(), + } + else if (d.delta.dy < (dragSensitivity * -1)) + { + showDetailsSheet(context, widget.file), + }, + }, + child: Center( + child: controller != null + ? common.VideoWidget( + widget.file, + controller!, + widget.playbackCallback, + isFromMemories: widget.isFromMemories, + onStreamChange: widget.onStreamChange, + ) + : const Center( + child: EnteLoadingWidget( + size: 32, + color: fillBaseDark, + padding: 0, + ), + ), + ), + ); + } + + void _setVideoController(String url) { + if (mounted) { + setState(() { + player.setPlaylistMode(PlaylistMode.single); + controller = VideoController(player); + player.open(Media(url), play: _isAppInFG); + }); + } + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 532ec80678..286c154ba7 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1587,7 +1587,7 @@ packages: source: git version: "1.0.2" media_kit: - dependency: "direct overridden" + dependency: "direct main" description: path: media_kit ref: HEAD @@ -1604,7 +1604,7 @@ packages: source: hosted version: "1.3.6" media_kit_libs_ios_video: - dependency: "direct overridden" + dependency: "direct main" description: path: "libs/ios/media_kit_libs_ios_video" ref: HEAD @@ -1629,7 +1629,7 @@ packages: source: hosted version: "1.1.4" media_kit_libs_video: - dependency: "direct overridden" + dependency: "direct main" description: path: "libs/universal/media_kit_libs_video" ref: HEAD @@ -1646,7 +1646,7 @@ packages: source: hosted version: "1.0.10" media_kit_video: - dependency: "direct overridden" + dependency: "direct main" description: path: media_kit_video ref: HEAD @@ -2802,7 +2802,7 @@ packages: source: git version: "3.0.0" video_player: - dependency: "direct overridden" + dependency: "direct main" description: path: "packages/video_player/video_player" ref: android_video_roation_fix @@ -2826,15 +2826,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" - video_player_media_kit: - dependency: "direct overridden" - description: - path: video_player_media_kit - ref: HEAD - resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80" - url: "https://github.com/media-kit/media-kit" - source: git - version: "1.0.5" video_player_platform_interface: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 7ad4a66164..d1179b13b3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -120,6 +120,22 @@ dependencies: git: url: "https://github.com/ente-io/media_extension.git" ref: deeplink_fixes + media_kit: + git: + url: https://github.com/media-kit/media-kit + path: media_kit + media_kit_libs_ios_video: + git: + url: https://github.com/media-kit/media-kit + path: libs/ios/media_kit_libs_ios_video + media_kit_libs_video: + git: + url: https://github.com/media-kit/media-kit + path: libs/universal/media_kit_libs_video + media_kit_video: + git: + url: https://github.com/media-kit/media-kit + path: media_kit_video ml_linalg: ^13.11.31 modal_bottom_sheet: ^3.0.0-pre motion_photos: @@ -186,6 +202,11 @@ dependencies: video_editor: git: url: https://github.com/prateekmedia/video_editor.git + video_player: + git: + url: https://github.com/ente-io/packages.git + ref: android_video_roation_fix + path: packages/video_player/video_player/ video_thumbnail: ^0.5.3 visibility_detector: ^0.3.3 wakelock_plus: ^1.1.1 @@ -200,32 +221,28 @@ dependency_overrides: # Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0 ffi: 2.1.0 intl: 0.18.1 + js: ^0.6.7 media_kit: git: url: https://github.com/media-kit/media-kit path: media_kit - media_kit_video: - git: - url: https://github.com/media-kit/media-kit - path: media_kit_video - media_kit_libs_video: - git: - url: https://github.com/media-kit/media-kit - path: libs/universal/media_kit_libs_video media_kit_libs_ios_video: git: url: https://github.com/media-kit/media-kit path: libs/ios/media_kit_libs_ios_video + media_kit_libs_video: + git: + url: https://github.com/media-kit/media-kit + path: libs/universal/media_kit_libs_video + media_kit_video: + git: + url: https://github.com/media-kit/media-kit + path: media_kit_video video_player: git: url: https://github.com/ente-io/packages.git ref: android_video_roation_fix path: packages/video_player/video_player/ - video_player_media_kit: - git: - url: https://github.com/media-kit/media-kit - path: video_player_media_kit - js: ^0.6.7 watcher: ^1.1.0 win32: ^5.5.4