feat: use media kit directly for preview, instead of video_player proxy

This commit is contained in:
Prateek Sunal 2025-02-20 15:23:21 +05:30
parent c37deecb96
commit c5dab37dfa
9 changed files with 1341 additions and 1182 deletions

View File

@ -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<void> _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");

View File

@ -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<PreviewVideoWidget> createState() => _PreviewVideoWidgetState();
}
// @override
// State<PreviewVideoWidget> createState() => _PreviewVideoWidgetState();
// }
class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
final _logger = Logger("PreviewVideoWidget");
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
final _progressNotifier = ValueNotifier<double?>(null);
bool _isPlaying = false;
final EnteWakeLock _wakeLock = EnteWakeLock();
bool _isFileSwipeLocked = false;
late final StreamSubscription<GuestViewEvent> _fileSwipeLockEventSubscription;
File? previewFile;
// class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
// final _logger = Logger("PreviewVideoWidget");
// VideoPlayerController? _videoPlayerController;
// ChewieController? _chewieController;
// final _progressNotifier = ValueNotifier<double?>(null);
// bool _isPlaying = false;
// final EnteWakeLock _wakeLock = EnteWakeLock();
// bool _isFileSwipeLocked = false;
// late final StreamSubscription<GuestViewEvent> _fileSwipeLockEventSubscription;
// File? previewFile;
@override
void initState() {
super.initState();
// @override
// void initState() {
// super.initState();
_checkForPreview();
_fileSwipeLockEventSubscription =
Bus.instance.on<GuestViewEvent>().listen((event) {
setState(() {
_isFileSwipeLocked = event.swipeLocked;
});
});
}
// _checkForPreview();
// _fileSwipeLockEventSubscription =
// Bus.instance.on<GuestViewEvent>().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<void> _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<void> _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>(
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>(
// 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<void> _keepScreenAliveOnPlaying(bool isPlaying) async {
if (isPlaying) {
_wakeLock.enable();
}
if (!isPlaying) {
_wakeLock.disable();
}
}
// Future<void> _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!),
// );
// }
// }

View File

@ -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<StatefulWidget> createState() {
return _VideoControlsState();
}
}
// @override
// State<StatefulWidget> createState() {
// return _VideoControlsState();
// }
// }
class _VideoControlsState extends State<VideoControls>
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<VideoControls>
// 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<PlayerNotifier>(context, listen: false);
}
// @override
// void initState() {
// super.initState();
// notifier = Provider.of<PlayerNotifier>(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: <Widget>[
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: <Widget>[
// 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<void> _initialize() async {
controller.addListener(_updateState);
// Future<void> _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<double> 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<double> 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,
// ),
// ),
// ),
// ),
// );
// }
// }

View File

@ -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<VideoWidget> {
?.containsKey(widget.file.uploadedFileID!) ??
false);
if (isPreviewVideoPlayable && selectPreviewForPlay) {
return PreviewVideoWidget(
return VideoWidgetMediaKitPreview(
widget.file,
tagPrefix: widget.tagPrefix,
playbackCallback: widget.playbackCallback,

View File

@ -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<VideoWidget> createState() => _VideoWidgetState();
}
class _VideoWidgetState extends State<VideoWidget> {
final showControlsNotifier = ValueNotifier<bool>(true);
static const verticalMargin = 72.0;
final _hideControlsDebouncer = Debouncer(
const Duration(milliseconds: 2000),
);
final _isSeekingNotifier = ValueNotifier<bool>(false);
late final StreamSubscription<bool> _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<PlayPauseButtonMediaKit> createState() => _PlayPauseButtonState();
}
class _PlayPauseButtonState extends State<PlayPauseButtonMediaKit> {
bool _isPlaying = true;
late final StreamSubscription<bool>? 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<double> 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<bool> 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<bool> isSeekingNotifier;
const SeekBar(
this.controller,
this.isSeekingNotifier, {
super.key,
});
@override
State<SeekBar> createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBar> {
double _sliderValue = 0.0;
late final StreamSubscription<Duration> _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,
),
);
}
}

View File

@ -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<VideoWidgetMediaKitNew>
},
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>(
// 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<VideoWidgetMediaKitNew>
}
}
}
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<bool>(true);
static const verticalMargin = 72.0;
final _hideControlsDebouncer = Debouncer(
const Duration(milliseconds: 2000),
);
final _isSeekingNotifier = ValueNotifier<bool>(false);
late final StreamSubscription<bool> _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<PlayPauseButtonMediaKit> createState() => _PlayPauseButtonState();
}
class _PlayPauseButtonState extends State<PlayPauseButtonMediaKit> {
bool _isPlaying = true;
late final StreamSubscription<bool>? 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<double> 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<bool> 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<bool> isSeekingNotifier;
const _SeekBar(
this.controller,
this.isSeekingNotifier,
);
@override
State<_SeekBar> createState() => _SeekBarState();
}
class _SeekBarState extends State<_SeekBar> {
double _sliderValue = 0.0;
late final StreamSubscription<Duration> _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,
),
);
}
}

View File

@ -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<VideoWidgetMediaKitPreview> createState() =>
_VideoWidgetMediaKitPreviewState();
}
class _VideoWidgetMediaKitPreviewState extends State<VideoWidgetMediaKitPreview>
with WidgetsBindingObserver {
final Logger _logger = Logger("VideoWidgetMediaKitNew");
late final player = Player();
VideoController? controller;
final _progressNotifier = ValueNotifier<double?>(null);
bool _isAppInFG = true;
late StreamSubscription<PauseVideoEvent> pauseVideoSubscription;
bool isGuestView = false;
late final StreamSubscription<GuestViewEvent> _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<PauseVideoEvent>().listen((event) {
player.pause();
});
_guestViewEventSubscription =
Bus.instance.on<GuestViewEvent>().listen((event) {
setState(() {
_isGuestView = event.isGuestView;
});
});
}
Future<void> _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);
});
}
}
}

View File

@ -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:

View File

@ -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