[mob] streaming patches (#5122)

## Description

Quality of Life fixes:
- [x] Queue fixes
- [x] Android Impeller fix
- [x] No video_player_media_kit proxy, just using media_kit directory

Quality of Dev fixes:
- [x] Use master branch of media_kit
- [x] extract common functions from native player and media kit for
seconds to duration.
This commit is contained in:
Prateek Sunal
2025-02-21 13:34:01 +05:30
committed by GitHub
14 changed files with 784 additions and 1387 deletions

View File

@@ -144,8 +144,6 @@ PODS:
- Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- motion_sensors (0.0.1):
@@ -189,8 +187,6 @@ PODS:
- PromisesObjC (2.4.0)
- receive_sharing_intent (1.8.0):
- Flutter
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
@@ -278,7 +274,6 @@ DEPENDENCIES:
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
- media_extension (from `.symlinks/plugins/media_extension/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- motion_sensors (from `.symlinks/plugins/motion_sensors/ios`)
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
@@ -293,7 +288,6 @@ DEPENDENCIES:
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- privacy_screen (from `.symlinks/plugins/privacy_screen/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -390,8 +384,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_extension/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
motion_sensors:
@@ -420,8 +412,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/privacy_screen/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
@@ -472,7 +462,7 @@ SPEC CHECKSUMS:
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: 35ddbc7228eafcb3969dcc5f1fbbe27c1145a4f0
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_sodium: 152647449ba89a157fd48d7e293dcd6d29c6ab0e
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
@@ -489,7 +479,6 @@ SPEC CHECKSUMS:
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
media_extension: a1fec16ee9c8241a6aef9613578ebf097d6c5e64
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
motionphoto: 584b43031ead3060225cdff08fa49818879801d2
@@ -509,7 +498,6 @@ SPEC CHECKSUMS:
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: f6a12b7e8f7ed745f61c982de8a65de88db44a44
screen_brightness_ios: 5ed898fa50fa82a26171c086ca5e28228f932576
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
@@ -526,7 +514,7 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
volume_controller: ca1cde542ee70fad77d388f82e9616488110942b
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd

View File

@@ -315,7 +315,6 @@
"${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework",
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
"${BUILT_PRODUCTS_DIR}/media_kit_libs_ios_video/media_kit_libs_ios_video.framework",
"${BUILT_PRODUCTS_DIR}/media_kit_native_event_loop/media_kit_native_event_loop.framework",
"${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework",
"${BUILT_PRODUCTS_DIR}/motion_sensors/motion_sensors.framework",
"${BUILT_PRODUCTS_DIR}/motionphoto/motionphoto.framework",
@@ -329,7 +328,6 @@
"${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework",
"${BUILT_PRODUCTS_DIR}/privacy_screen/privacy_screen.framework",
"${BUILT_PRODUCTS_DIR}/receive_sharing_intent/receive_sharing_intent.framework",
"${BUILT_PRODUCTS_DIR}/screen_brightness_ios/screen_brightness_ios.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
@@ -412,7 +410,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_libs_ios_video.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_native_event_loop.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motion_sensors.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motionphoto.framework",
@@ -426,7 +423,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/privacy_screen.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/receive_sharing_intent.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/screen_brightness_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",

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

@@ -58,10 +58,9 @@ class PreviewVideoStore {
void init(SharedPreferences prefs) {
_prefs = prefs;
Future.delayed(
const Duration(seconds: 10),
_putFilesForPreviewCreation,
);
FileDataService.instance.syncFDStatus().then(
(_) => _putFilesForPreviewCreation(),
);
}
late final SharedPreferences _prefs;
@@ -74,11 +73,13 @@ class PreviewVideoStore {
Future<void> setIsVideoStreamingEnabled(bool value) async {
final oneMonthBack = DateTime.now().subtract(const Duration(days: 30));
await _prefs.setBool(_videoStreamingEnabled, value);
await _prefs.setInt(
_videoStreamingCutoff,
oneMonthBack.millisecondsSinceEpoch,
);
_prefs.setBool(_videoStreamingEnabled, value).ignore();
_prefs
.setInt(
_videoStreamingCutoff,
oneMonthBack.millisecondsSinceEpoch,
)
.ignore();
Bus.instance.fire(VideoStreamingChanged());
if (isVideoStreamingEnabled) {
@@ -664,23 +665,24 @@ class PreviewVideoStore {
}).toList();
// set all video status to in queue
for (final enteFile in allFiles) {
final n = allFiles.length;
for (int i = 0; i < n; i++) {
final enteFile = allFiles[i];
// elimination case for <=10 MB with H.264
final (_, result, _) = await _checkFileForPreviewCreation(enteFile);
if (result) {
allFiles.remove(enteFile);
continue;
allFiles.removeAt(i);
} else {
_items[enteFile.uploadedFileID!] = PreviewItem(
status: PreviewItemStatus.inQueue,
file: enteFile,
collectionID: enteFile.collectionID ?? 0,
);
}
_items[enteFile.uploadedFileID!] = PreviewItem(
status: PreviewItemStatus.inQueue,
file: enteFile,
collectionID: enteFile.collectionID ?? 0,
);
}
Bus.instance.fire(PreviewUpdatedEvent(_items));
_logger.info("[init] Processing ${_items.length} items for streaming");
_logger.info("[init] Processing ${allFiles.length} items for streaming");
// take first file and put it for stream generation
final file = allFiles.removeAt(0);

View File

@@ -1,266 +0,0 @@
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';
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,
});
@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;
@override
void initState() {
super.initState();
_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();
}
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!);
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,
),
);
}
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,
),
);
}
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!),
);
}
}

View File

@@ -1,454 +0,0 @@
// ignore_for_file: implementation_imports
import 'dart:async';
import "package:chewie/chewie.dart";
import "package:chewie/src/helpers/utils.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: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;
@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;
final barHeight = 48.0 * 1.5;
final marginSize = 5.0;
late VideoPlayerController controller;
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
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),
],
),
),
],
),
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
_initTimer?.cancel();
_showAfterExpandCollapseTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
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),
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(
formatDuration(_latestValue.position),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildProgressBar(),
),
const SizedBox(width: 16),
Text(
formatDuration(
_latestValue.duration,
),
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;
return GestureDetector(
onTap: () {
if (_latestValue.isPlaying) {
if (_displayTapped) {
setState(() {
notifier.hideStuff = true;
});
} else {
_cancelAndRestartTimer();
}
} else {
_playPause();
setState(() {
notifier.hideStuff = true;
});
}
widget.playbackCallback?.call(notifier.hideStuff);
},
child: Container(
alignment: Alignment.center,
color: Colors
.transparent, // The Gesture Detector doesn't expand to the full size of the container without this; Not sure why!
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();
setState(() {
notifier.hideStuff = false;
_displayTapped = true;
});
widget.playbackCallback?.call(notifier.hideStuff);
}
Future<void> _initialize() async {
controller.addListener(_updateState);
_updateState();
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);
});
}
}
void _playPause() {
final bool isFinished = (_latestValue.position >= _latestValue.duration) &&
_latestValue.duration.inSeconds > 0;
setState(() {
if (controller.value.isPlaying) {
notifier.hideStuff = false;
widget.playbackCallback?.call(notifier.hideStuff);
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
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 _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;
}
setState(() {
_latestValue = controller.value;
});
}
Widget _buildProgressBar() {
final colorScheme = getEnteColorScheme(context);
return Expanded(
child: CustomProgressBar(
controller,
onDragStart: () {
setState(() {
_dragging = true;
});
_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,
),
);
}
}
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;
@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/date_time_util.dart";
import "package:photos/utils/debouncer.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,10 +14,10 @@ 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/toast_util.dart";
@@ -27,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,
});
@@ -137,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,
),
),
),
);
}
@@ -229,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

@@ -24,6 +24,7 @@ import "package:photos/ui/viewer/file/native_video_player_controls/play_pause_bu
import "package:photos/ui/viewer/file/native_video_player_controls/seek_bar.dart";
import "package:photos/ui/viewer/file/preview_status_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/utils/date_time_util.dart";
import "package:photos/utils/debouncer.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/exif_util.dart";
@@ -615,9 +616,7 @@ class _SeekBarAndDuration extends StatelessWidget {
_,
) {
return Text(
_secondsToDuration(
value,
),
secondsToDuration(value),
style: getEnteTextTheme(
context,
).mini.copyWith(
@@ -630,17 +629,13 @@ class _SeekBarAndDuration extends StatelessWidget {
Expanded(
child: SeekBar(
controller!,
_durationToSeconds(
duration,
),
durationToSeconds(duration),
isSeeking,
),
),
Text(
duration ?? "0:00",
style: getEnteTextTheme(
context,
).mini.copyWith(
style: getEnteTextTheme(context).mini.copyWith(
color: textBaseDark,
),
),
@@ -653,43 +648,6 @@ class _SeekBarAndDuration extends StatelessWidget {
},
);
}
/// 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')}';
}
}
/// Returns the duration in seconds from the format "h:mm:ss" or "m:ss".
int? _durationToSeconds(String? duration) {
if (duration == null) {
return null;
}
final parts = duration.split(':');
int seconds = 0;
if (parts.length == 3) {
// Format: "h:mm:ss"
seconds += int.parse(parts[0]) * 3600; // Hours to seconds
seconds += int.parse(parts[1]) * 60; // Minutes to seconds
seconds += int.parse(parts[2]); // Seconds
} else if (parts.length == 2) {
// Format: "m:ss"
seconds += int.parse(parts[0]) * 60; // Minutes to seconds
seconds += int.parse(parts[1]); // Seconds
} else {
throw FormatException('Invalid duration format: $duration');
}
return seconds;
}
}
class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget {

View File

@@ -216,3 +216,40 @@ bool isNumeric(String? s) {
}
return double.tryParse(s) != null;
}
/// Returns the duration in seconds from the format "h:mm:ss" or "m:ss".
int? durationToSeconds(String? duration) {
if (duration == null) {
return null;
}
final parts = duration.split(':');
int seconds = 0;
if (parts.length == 3) {
// Format: "h:mm:ss"
seconds += int.parse(parts[0]) * 3600; // Hours to seconds
seconds += int.parse(parts[1]) * 60; // Minutes to seconds
seconds += int.parse(parts[2]); // Seconds
} else if (parts.length == 2) {
// Format: "m:ss"
seconds += int.parse(parts[0]) * 60; // Minutes to seconds
seconds += int.parse(parts[1]); // Seconds
} else {
throw FormatException('Invalid duration format: $duration');
}
return seconds;
}
/// 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')}';
}
}

View File

@@ -977,26 +977,26 @@ packages:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f"
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "8.1.0"
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
@@ -1017,10 +1017,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255"
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "3.1.2"
flutter_shaders:
dependency: transitive
description:
@@ -1386,7 +1386,7 @@ packages:
source: hosted
version: "0.10.1"
js:
dependency: transitive
dependency: "direct overridden"
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
@@ -1589,10 +1589,11 @@ packages:
media_kit:
dependency: "direct main"
description:
name: media_kit
sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62"
url: "https://pub.dev"
source: hosted
path: media_kit
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.11"
media_kit_libs_android_video:
dependency: transitive
@@ -1605,10 +1606,11 @@ packages:
media_kit_libs_ios_video:
dependency: "direct main"
description:
name: media_kit_libs_ios_video
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
url: "https://pub.dev"
source: hosted
path: "libs/ios/media_kit_libs_ios_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.4"
media_kit_libs_linux:
dependency: transitive
@@ -1629,10 +1631,11 @@ packages:
media_kit_libs_video:
dependency: "direct main"
description:
name: media_kit_libs_video
sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288"
url: "https://pub.dev"
source: hosted
path: "libs/universal/media_kit_libs_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.0.5"
media_kit_libs_windows_video:
dependency: transitive
@@ -1642,21 +1645,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.10"
media_kit_native_event_loop:
dependency: transitive
description:
name: media_kit_native_event_loop
sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
media_kit_video:
dependency: "direct main"
description:
name: media_kit_video
sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f"
url: "https://pub.dev"
source: hosted
path: media_kit_video
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.2.5"
meta:
dependency: transitive
@@ -2171,58 +2167,26 @@ packages:
dependency: transitive
description:
name: safe_local_storage
sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440
sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236
url: "https://pub.dev"
source: hosted
version: "1.0.2"
screen_brightness:
dependency: transitive
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
version: "2.0.1"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf"
sha256: ff9141bed547db02233e7dd88f990ab01973a0c8a8c04ddb855c7b072f33409a
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
version: "2.1.0"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171
sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
version: "2.1.0"
screenshot:
dependency: "direct main"
description:
@@ -2712,10 +2676,10 @@ packages:
dependency: transitive
description:
name: uri_parser
sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835"
sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "3.0.0"
url_launcher:
dependency: "direct main"
description:
@@ -2862,14 +2826,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.2"
video_player_media_kit:
dependency: "direct main"
description:
name: video_player_media_kit
sha256: eadf78b85d0ecc6f65bb5ca84c5ad9546a8609c6c0ee207e81673f7969461f3b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
video_player_platform_interface:
dependency: transitive
description:
@@ -2914,10 +2870,10 @@ packages:
dependency: transitive
description:
name: volume_controller
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e
sha256: "68975792fbd2ac36cd2aa387273669772a452785d94b6db5808bf2dc3e44a0f1"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
version: "3.3.0"
wakelock_plus:
dependency: "direct main"
description:

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.9.99+1002
version: 0.9.99+1003
publish_to: none
environment:
@@ -90,7 +90,7 @@ dependencies:
flutter_map_marker_cluster: ^1.3.6
flutter_native_splash: ^2.2.0+1
flutter_password_strength: ^0.1.6
flutter_secure_storage: ^8.0.0
flutter_secure_storage: ^9.2.4
flutter_sodium: ^0.2.0
flutter_staggered_grid_view: ^0.6.2
flutter_svg: ^2.0.10+1
@@ -120,10 +120,22 @@ dependencies:
git:
url: "https://github.com/ente-io/media_extension.git"
ref: deeplink_fixes
media_kit: ^1.1.10+1
media_kit_libs_ios_video: ^1.1.4
media_kit_libs_video: ^1.0.4
media_kit_video: ^1.2.4
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:
@@ -195,7 +207,6 @@ dependencies:
url: https://github.com/ente-io/packages.git
ref: android_video_roation_fix
path: packages/video_player/video_player/
video_player_media_kit: ^1.0.5
video_thumbnail: ^0.5.3
visibility_detector: ^0.3.3
wakelock_plus: ^1.1.1
@@ -210,6 +221,23 @@ 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_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