[mob] Fix: Take wakelock to continue video download (#6234)

## Description
Note: Review by commit, we have just removed old widget and renamed the
media_kit_new to media_kit.

Previously, while using media_kit, as we were not taking wakeload before
video playback actually starts, the download was getting paused when
dispose was getting called.

Also, increased the file size limit for internal users.



## Tests
This commit is contained in:
Neeraj
2025-06-10 17:39:06 +05:30
committed by GitHub
4 changed files with 169 additions and 527 deletions

View File

@@ -15,7 +15,7 @@ import "package:photos/services/preview_video_store.dart";
import "package:photos/theme/colors.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/notification/toast.dart";
import "package:photos/ui/viewer/file/video_widget_media_kit_new.dart";
import "package:photos/ui/viewer/file/video_widget_media_kit.dart";
import "package:photos/ui/viewer/file/video_widget_native.dart";
import "package:photos/utils/standalone/data.dart";
@@ -164,7 +164,7 @@ class _VideoWidgetState extends State<VideoWidget> {
},
);
}
return VideoWidgetMediaKitNew(
return VideoWidgetMediaKit(
widget.file,
key: mediaKitKey,
tagPrefix: widget.tagPrefix,

View File

@@ -1,24 +1,30 @@
import "dart:async";
import "dart:io";
import "package:flutter/cupertino.dart";
import "package:flutter/material.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/file_caption_updated_event.dart";
import "package:photos/events/guest_view_event.dart";
import "package:photos/events/pause_video_event.dart";
import "package:photos/events/stream_switched_event.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/file/extensions/file_props.dart";
import "package:photos/models/file/file.dart";
import "package:photos/module/download/task.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/files_service.dart";
import "package:photos/services/wake_lock_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/common/loading_widget.dart";
import "package:photos/ui/notification/toast.dart";
import "package:photos/ui/viewer/file/thumbnail_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";
@@ -26,10 +32,19 @@ class VideoWidgetMediaKit extends StatefulWidget {
final EnteFile file;
final String? tagPrefix;
final Function(bool)? playbackCallback;
final bool isFromMemories;
final void Function() onStreamChange;
final File? preview;
final bool selectedPreview;
const VideoWidgetMediaKit(
this.file, {
this.tagPrefix,
this.playbackCallback,
this.isFromMemories = false,
required this.onStreamChange,
this.preview,
required this.selectedPreview,
super.key,
});
@@ -39,16 +54,19 @@ class VideoWidgetMediaKit extends StatefulWidget {
class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
with WidgetsBindingObserver {
final Logger _logger = Logger("VideoWidgetMediaKit");
static const verticalMargin = 72.0;
final Logger _logger = Logger("VideoWidgetMediaKitNew");
late final player = Player();
VideoController? controller;
final _progressNotifier = ValueNotifier<double?>(null);
late StreamSubscription<bool> playingStreamSubscription;
bool _isAppInFG = true;
late StreamSubscription<PauseVideoEvent> pauseVideoSubscription;
bool isGuestView = false;
late final StreamSubscription<GuestViewEvent> _guestViewEventSubscription;
bool _isGuestView = false;
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
StreamSubscription<DownloadTask>? _downloadTaskSubscription;
late final StreamSubscription<FileCaptionUpdatedEvent>
_captionUpdatedSubscription;
@override
void initState() {
@@ -57,6 +75,61 @@ class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
);
super.initState();
WidgetsBinding.instance.addObserver(this);
if (widget.selectedPreview) {
loadPreview();
} else {
loadOriginal();
}
pauseVideoSubscription = Bus.instance.on<PauseVideoEvent>().listen((event) {
player.pause();
});
_guestViewEventSubscription =
Bus.instance.on<GuestViewEvent>().listen((event) {
setState(() {
_isGuestView = event.isGuestView;
});
});
if (widget.file.isUploaded) {
_downloadTaskSubscription = downloadManager
.watchDownload(widget.file.uploadedFileID!)
.listen((event) {
if (mounted) {
setState(() {
_progressNotifier.value = event.progress;
});
}
});
}
_streamSwitchedSubscription =
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
if (event.type != PlayerType.mediaKit || !mounted) return;
if (event.selectedPreview) {
loadPreview();
} else {
loadOriginal();
}
});
_captionUpdatedSubscription =
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
if (event.fileGeneratedID == widget.file.generatedID) {
if (mounted) {
setState(() {});
}
}
});
EnteWakeLockService.instance
.updateWakeLock(enable: true, wakeLockFor: WakeLockFor.videoPlayback);
}
void loadPreview() {
_setVideoController(widget.preview!.path);
}
void loadOriginal() {
if (widget.file.isRemoteFile) {
_loadNetworkVideo();
_setFileSizeIfNull();
@@ -84,21 +157,6 @@ class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
}
});
}
playingStreamSubscription = player.stream.playing.listen((event) {
if (widget.playbackCallback != null && mounted) {
widget.playbackCallback!(event);
}
});
pauseVideoSubscription = Bus.instance.on<PauseVideoEvent>().listen((event) {
player.pause();
});
_guestViewEventSubscription =
Bus.instance.on<GuestViewEvent>().listen((event) {
setState(() {
isGuestView = event.isGuestView;
});
});
}
@override
@@ -112,71 +170,93 @@ class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
@override
void dispose() {
_streamSwitchedSubscription?.cancel();
_guestViewEventSubscription.cancel();
pauseVideoSubscription.cancel();
removeCallBack(widget.file);
_progressNotifier.dispose();
WidgetsBinding.instance.removeObserver(this);
playingStreamSubscription.cancel();
if (_downloadTaskSubscription != null) {
_downloadTaskSubscription!.cancel();
downloadManager.pause(widget.file.uploadedFileID!).ignore();
}
player.dispose();
_captionUpdatedSubscription.cancel();
if (EnteWakeLockService.instance.shouldKeepAppAwakeAcrossSessions) {
EnteWakeLockService.instance.updateWakeLock(
enable: true,
wakeLockFor: WakeLockFor.handlingMediaKitEdgeCase,
);
} else {
EnteWakeLockService.instance.updateWakeLock(
enable: false,
wakeLockFor: WakeLockFor.videoPlayback,
);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Hero(
tag: widget.tagPrefix! + widget.file.tag,
child: MaterialVideoControlsTheme(
normal: MaterialVideoControlsThemeData(
backdropColor: null,
automaticallyImplySkipNextButton: false,
automaticallyImplySkipPreviousButton: false,
seekOnDoubleTap: false,
displaySeekBar: true,
seekBarMargin: const EdgeInsets.only(bottom: verticalMargin),
bottomButtonBarMargin: const EdgeInsets.only(bottom: 112),
controlsHoverDuration: const Duration(seconds: 3),
seekBarHeight: 2,
seekBarThumbSize: 16,
seekBarBufferColor: Colors.transparent,
seekBarThumbColor: backgroundElevatedLight,
seekBarColor: fillMutedDark,
seekBarPositionColor: backgroundElevatedLight,
seekBarContainerHeight: 56,
seekBarAlignment: Alignment.center,
///topButtonBarMargin is needed for keeping the buffering loading
///indicator to be center aligned
topButtonBarMargin: const EdgeInsets.only(top: verticalMargin),
bottomButtonBar: [
const Spacer(),
PausePlayAndDuration(controller?.player),
const Spacer(),
],
primaryButtonBar: [],
),
fullscreen: const MaterialVideoControlsThemeData(),
child: 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),
},
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
? Video(
controller: controller!,
)
: _getLoadingWidget(),
),
),
},
child: Center(
child: controller != null
? common.VideoWidget(
widget.file,
controller!,
widget.playbackCallback,
isFromMemories: widget.isFromMemories,
onStreamChange: widget.onStreamChange,
isPreviewPlayer: widget.selectedPreview,
)
: Center(
child: ValueListenableBuilder(
valueListenable: _progressNotifier,
builder: (BuildContext context, double? progress, _) {
return progress == null || progress == 1
? const EnteLoadingWidget(
size: 32,
color: fillBaseDark,
padding: 0,
)
: Stack(
children: [
CircularProgressIndicator(
backgroundColor: Colors.transparent,
value: progress,
valueColor: const AlwaysStoppedAnimation<Color>(
Color.fromRGBO(45, 194, 98, 1.0),
),
strokeWidth: 2,
strokeCap: StrokeCap.round,
),
if (flagService.internalUser)
Center(
child: Text(
"${(progress * 100).toStringAsFixed(0)}%",
style:
getEnteTextTheme(context).tiny.copyWith(
color: textBaseDark,
),
),
),
],
);
},
),
),
),
);
}
@@ -221,148 +301,19 @@ class _VideoWidgetMediaKitState extends State<VideoWidgetMediaKit>
}
}
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,
),
);
}
void _setVideoController(String url) {
if (mounted) {
setState(() {
player.setPlaylistMode(PlaylistMode.single);
controller = VideoController(player);
if (controller == null) {
player.setPlaylistMode(
localSettings.shouldLoopVideo()
? PlaylistMode.single
: PlaylistMode.none,
);
controller = VideoController(player);
}
player.open(Media(url), play: _isAppInFG);
});
}
}
}
class PausePlayAndDuration extends StatefulWidget {
final Player? player;
const PausePlayAndDuration(this.player, {super.key});
@override
State<PausePlayAndDuration> createState() => _PausePlayAndDurationState();
}
class _PausePlayAndDurationState extends State<PausePlayAndDuration> {
Color backgroundColor = fillStrongLight;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) {
setState(() {
backgroundColor = fillMutedDark;
});
},
onTapUp: (details) {
Future.delayed(const Duration(milliseconds: 175), () {
if (mounted) {
setState(() {
backgroundColor = fillStrongLight;
});
}
});
},
onTapCancel: () {
Future.delayed(const Duration(milliseconds: 175), () {
if (mounted) {
setState(() {
backgroundColor = fillStrongLight;
});
}
});
},
onTap: () => widget.player!.playOrPause(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInBack,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(
color: strokeFaintDark,
width: 1,
),
borderRadius: BorderRadius.circular(24),
),
child: AnimatedSize(
duration: const Duration(seconds: 2),
curve: Curves.easeInOutExpo,
child: Row(
children: [
StreamBuilder(
builder: (context, snapshot) {
final bool isPlaying = snapshot.data ?? false;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
switchInCurve: Curves.easeInOutCirc,
switchOutCurve: Curves.easeInOutCirc,
child: Icon(
key: ValueKey(
isPlaying ? "pause_button" : "play_button",
),
isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
color: backdropBaseLight,
size: 24,
),
);
},
initialData: widget.player?.state.playing,
stream: widget.player?.stream.playing,
),
const SizedBox(width: 8),
MaterialPositionIndicator(
style: getEnteTextTheme(context).tiny.copyWith(
color: textBaseDark,
),
),
const SizedBox(width: 10),
],
),
),
),
);
}
}

View File

@@ -1,312 +0,0 @@
import "dart:async";
import "dart:io";
import "package:flutter/material.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/file_caption_updated_event.dart";
import "package:photos/events/guest_view_event.dart";
import "package:photos/events/pause_video_event.dart";
import "package:photos/events/stream_switched_event.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/file/extensions/file_props.dart";
import "package:photos/models/file/file.dart";
import "package:photos/module/download/task.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/files_service.dart";
import "package:photos/services/wake_lock_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/common/loading_widget.dart";
import "package:photos/ui/notification/toast.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";
class VideoWidgetMediaKitNew extends StatefulWidget {
final EnteFile file;
final String? tagPrefix;
final Function(bool)? playbackCallback;
final bool isFromMemories;
final void Function() onStreamChange;
final File? preview;
final bool selectedPreview;
const VideoWidgetMediaKitNew(
this.file, {
this.tagPrefix,
this.playbackCallback,
this.isFromMemories = false,
required this.onStreamChange,
this.preview,
required this.selectedPreview,
super.key,
});
@override
State<VideoWidgetMediaKitNew> createState() => _VideoWidgetMediaKitNewState();
}
class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
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;
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
StreamSubscription<DownloadTask>? _downloadTaskSubscription;
late final StreamSubscription<FileCaptionUpdatedEvent>
_captionUpdatedSubscription;
@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);
if (widget.selectedPreview) {
loadPreview();
} else {
loadOriginal();
}
pauseVideoSubscription = Bus.instance.on<PauseVideoEvent>().listen((event) {
player.pause();
});
_guestViewEventSubscription =
Bus.instance.on<GuestViewEvent>().listen((event) {
setState(() {
_isGuestView = event.isGuestView;
});
});
if (widget.file.isUploaded) {
_downloadTaskSubscription = downloadManager
.watchDownload(widget.file.uploadedFileID!)
.listen((event) {
if (mounted) {
setState(() {
_progressNotifier.value = event.progress;
});
}
});
}
_streamSwitchedSubscription =
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
if (event.type != PlayerType.mediaKit || !mounted) return;
if (event.selectedPreview) {
loadPreview();
} else {
loadOriginal();
}
});
_captionUpdatedSubscription =
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
if (event.fileGeneratedID == widget.file.generatedID) {
if (mounted) {
setState(() {});
}
}
});
}
void loadPreview() {
_setVideoController(widget.preview!.path);
}
void loadOriginal() {
if (widget.file.isRemoteFile) {
_loadNetworkVideo();
_setFileSizeIfNull();
} else if (widget.file.isSharedMediaToAppSandbox) {
final localFile = File(getSharedMediaFilePath(widget.file));
if (localFile.existsSync()) {
_setVideoController(localFile.path);
} else if (widget.file.uploadedFileID != null) {
_loadNetworkVideo();
}
} else {
widget.file.getAsset.then((asset) async {
if (asset == null || !(await asset.exists)) {
if (widget.file.uploadedFileID != null) {
_loadNetworkVideo();
}
} else {
// ignore: unawaited_futures
asset.getMediaUrl().then((url) {
_setVideoController(
url ??
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
);
});
}
});
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_isAppInFG = true;
} else {
_isAppInFG = false;
}
}
@override
void dispose() {
_streamSwitchedSubscription?.cancel();
_guestViewEventSubscription.cancel();
pauseVideoSubscription.cancel();
removeCallBack(widget.file);
_progressNotifier.dispose();
WidgetsBinding.instance.removeObserver(this);
if (_downloadTaskSubscription != null) {
_downloadTaskSubscription!.cancel();
downloadManager.pause(widget.file.uploadedFileID!).ignore();
}
player.dispose();
_captionUpdatedSubscription.cancel();
if (EnteWakeLockService.instance.shouldKeepAppAwakeAcrossSessions) {
EnteWakeLockService.instance.updateWakeLock(
enable: true,
wakeLockFor: WakeLockFor.handlingMediaKitEdgeCase,
);
}
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,
isPreviewPlayer: widget.selectedPreview,
)
: Center(
child: ValueListenableBuilder(
valueListenable: _progressNotifier,
builder: (BuildContext context, double? progress, _) {
return progress == null || progress == 1
? const EnteLoadingWidget(
size: 32,
color: fillBaseDark,
padding: 0,
)
: Stack(
children: [
CircularProgressIndicator(
backgroundColor: Colors.transparent,
value: progress,
valueColor: const AlwaysStoppedAnimation<Color>(
Color.fromRGBO(45, 194, 98, 1.0),
),
strokeWidth: 2,
strokeCap: StrokeCap.round,
),
if (flagService.internalUser)
Center(
child: Text(
"${(progress * 100).toStringAsFixed(0)}%",
style:
getEnteTextTheme(context).tiny.copyWith(
color: textBaseDark,
),
),
),
],
);
},
),
),
),
);
}
void _loadNetworkVideo() {
getFileFromServer(
widget.file,
progressCallback: (count, total) {
if (!mounted) {
return;
}
_progressNotifier.value = count / (widget.file.fileSize ?? total);
if (_progressNotifier.value == 1) {
if (mounted) {
showShortToast(context, S.of(context).decryptingVideo);
}
}
},
).then((file) {
if (file != null) {
_setVideoController(file.path);
}
}).onError((error, stackTrace) {
showErrorDialog(
context,
S.of(context).error,
S.of(context).failedToDownloadVideo,
);
});
}
void _setFileSizeIfNull() {
if (widget.file.fileSize == null && widget.file.canEditMetaInfo) {
FilesService.instance
.getFileSize(widget.file.uploadedFileID!)
.then((value) {
widget.file.fileSize = value;
if (mounted) {
setState(() {});
}
});
}
}
void _setVideoController(String url) {
if (mounted) {
setState(() {
if (controller == null) {
player.setPlaylistMode(
localSettings.shouldLoopVideo()
? PlaylistMode.single
: PlaylistMode.none,
);
controller = VideoController(player);
}
player.open(Media(url), play: _isAppInFG);
});
}
}
}

View File

@@ -54,6 +54,7 @@ class FileUploader {
static const kMaximumThumbnailCompressionAttempts = 2;
static const kMaximumUploadAttempts = 4;
static const kMaxFileSize5Gib = 5368709120;
static const kMaxFileSize10Gib = 10737418240;
static const kBlockedUploadsPollFrequency = Duration(seconds: 2);
static const kFileUploadTimeout = Duration(minutes: 50);
static const k20MBStorageBuffer = 20 * 1024 * 1024;
@@ -1086,10 +1087,12 @@ class FileUploader {
'freeStorage $freeStorage');
throw StorageLimitExceededError();
}
if (fileSize > kMaxFileSize5Gib) {
_logger.warning('File size exceeds 5GiB fileSize $fileSize');
final int maxSize =
flagService.internalUser ? kMaxFileSize10Gib : kMaxFileSize5Gib;
if (fileSize > maxSize) {
_logger.warning('File size exceeds $maxSize fileSize $fileSize');
throw InvalidFileError(
'file size above 5GiB',
'file size above $maxSize',
InvalidReason.tooLargeFile,
);
}