feat(mobile): init preview video branch

This commit is contained in:
Prateek Sunal
2024-07-26 19:52:12 +05:30
parent 8072b2943a
commit b2d2f0d76d
5 changed files with 492 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ const latKey = "lat";
const longKey = "long";
const motionVideoIndexKey = "mvi";
const noThumbKey = "noThumb";
const previewVersionKey = "previewVersion";
class MagicMetadata {
// 0 -> visible

View File

@@ -1,12 +1,9 @@
import "dart:io";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/ui/viewer/file/video_widget.dart";
import "package:photos/ui/viewer/file/video_widget_new.dart";
import "package:photos/ui/viewer/file/video_view_widget.dart";
import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
class FileWidget extends StatelessWidget {
@@ -42,16 +39,7 @@ class FileWidget extends StatelessWidget {
key: key ?? ValueKey(fileKey),
);
} else if (file.fileType == FileType.video) {
// use old video widget on iOS simulator as the new one crashes while
// playing certain videos on iOS simulator
if (kDebugMode && Platform.isIOS) {
return VideoWidget(
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,
);
}
return VideoWidgetNew(
return VideoViewWidget(
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,

View File

@@ -0,0 +1,299 @@
import 'dart:async';
import 'dart:io';
import 'package:chewie/chewie.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
import "package:photos/core/event_bus.dart";
import "package:photos/events/file_swipe_lock_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/service_locator.dart";
import 'package:photos/services/files_service.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_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;
const PreviewVideoWidget(
this.file, {
this.autoPlay = false,
this.tagPrefix,
this.playbackCallback,
Key? key,
}) : super(key: key);
@override
State<PreviewVideoWidget> createState() => _PreviewVideoWidgetState();
}
class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
final _logger = Logger("VideoWidget");
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
final _progressNotifier = ValueNotifier<double?>(null);
bool _isPlaying = false;
final EnteWakeLock _wakeLock = EnteWakeLock();
bool _isFileSwipeLocked = false;
late final StreamSubscription<FileSwipeLockEvent>
_fileSwipeLockEventSubscription;
@override
void initState() {
super.initState();
if (widget.file.isRemoteFile) {
_loadNetworkVideo();
_setFileSizeIfNull();
} else if (widget.file.isSharedMediaToAppSandbox) {
final localFile = File(getSharedMediaFilePath(widget.file));
if (localFile.existsSync()) {
_logger.fine("loading from app cache");
_setVideoPlayerController(file: localFile);
} 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) {
_setVideoPlayerController(url: url);
});
}
});
}
_fileSwipeLockEventSubscription =
Bus.instance.on<FileSwipeLockEvent>().listen((event) {
setState(() {
_isFileSwipeLocked = event.shouldSwipeLock;
});
});
}
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 _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 && mounted) {
_setVideoPlayerController(file: file);
}
}).onError((error, stackTrace) {
if (mounted) {
showErrorDialog(
context,
"Error",
S.of(context).failedToDownloadVideo,
);
}
});
}
@override
void dispose() {
_fileSwipeLockEventSubscription.cancel();
removeCallBack(widget.file);
_videoPlayerController?.dispose();
_chewieController?.dispose();
_progressNotifier.dispose();
_wakeLock.dispose();
super.dispose();
}
void _setVideoPlayerController({
String? url,
File? file,
}) {
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;
if (url != null) {
videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(url));
} else {
videoPlayerController = VideoPlayerController.file(file!);
}
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(
videoPlayerController: _videoPlayerController!,
aspectRatio: _videoPlayerController!.value.aspectRatio,
autoPlay: widget.autoPlay!,
autoInitialize: true,
looping: true,
allowMuting: true,
allowFullScreen: false,
customControls: const VideoControls(),
);
return Container(
color: Colors.black,
child: Chewie(controller: _chewieController!),
);
}
}

View File

@@ -0,0 +1,151 @@
// ignore_for_file: unused_import
import 'dart:async';
import 'dart:io';
import 'package:chewie/chewie.dart';
import 'package:flutter/cupertino.dart';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
import "package:photos/core/event_bus.dart";
import "package:photos/events/file_swipe_lock_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/service_locator.dart";
import "package:photos/ui/actions/file/file_actions.dart";
import "package:photos/ui/viewer/file/preview_video_widget.dart";
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/video_controls.dart';
import "package:photos/ui/viewer/file/video_widget.dart";
import "package:photos/ui/viewer/file/video_widget_new.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 VideoViewWidget extends StatefulWidget {
final EnteFile file;
final bool? autoPlay;
final String? tagPrefix;
final Function(bool)? playbackCallback;
const VideoViewWidget(
this.file, {
this.autoPlay = false,
this.tagPrefix,
this.playbackCallback,
super.key,
});
@override
State<VideoViewWidget> createState() => _VideoViewWidgetState();
}
class _VideoViewWidgetState extends State<VideoViewWidget> {
final _logger = Logger("VideoViewWidget");
bool isCheckingForPreview = true;
File? previewFile;
@override
void initState() {
super.initState();
_checkForPreview();
}
void _checkForPreview() {
if (!flagService.internalUser) return;
getPreviewFileFromServer(
widget.file,
).then((file) {
if (!mounted) return;
if (file != null) {
isCheckingForPreview = false;
previewFile = file;
setState(() {});
} else {
isCheckingForPreview = false;
setState(() {});
}
}).onError((error, stackTrace) {
if (!mounted) return;
_logger.warning("Failed to download preview video", error, stackTrace);
showErrorDialog(
context,
"Error",
S.of(context).failedToDownloadVideo,
);
isCheckingForPreview = false;
setState(() {});
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
if (flagService.internalUser) {
if (isCheckingForPreview) {
return _getLoadingWidget();
}
if (previewFile != null) {
return PreviewVideoWidget(
widget.file,
tagPrefix: widget.tagPrefix,
playbackCallback: widget.playbackCallback,
);
}
}
if (kDebugMode && Platform.isIOS) {
return VideoWidget(
widget.file,
tagPrefix: widget.tagPrefix,
playbackCallback: widget.playbackCallback,
);
}
return VideoWidgetNew(
widget.file,
tagPrefix: widget.tagPrefix,
playbackCallback: widget.playbackCallback,
);
}
Widget _getLoadingWidget() {
return Stack(
children: [
_getThumbnail(),
Container(
color: Colors.black12,
constraints: const BoxConstraints.expand(),
),
Center(
child: SizedBox.fromSize(
size: const Size.square(20),
child: const CupertinoActivityIndicator(
color: Colors.white,
),
),
),
],
);
}
Widget _getThumbnail() {
return Container(
color: Colors.black,
constraints: const BoxConstraints.expand(),
child: ThumbnailWidget(
widget.file,
fit: BoxFit.contain,
),
);
}
}

View File

@@ -130,6 +130,45 @@ void removeCallBack(EnteFile file) {
}
}
Future<File?> getPreviewFileFromServer(
EnteFile file, {
ProgressCallback? progressCallback,
}) async {
final cacheManager = DefaultCacheManager();
final fileFromCache = await cacheManager.getFileFromCache(file.downloadUrl);
if (fileFromCache != null) {
return fileFromCache.file;
}
final downloadID = file.uploadedFileID.toString();
if (progressCallback != null) {
_progressCallbacks[downloadID] = progressCallback;
}
if (!_fileDownloadsInProgress.containsKey(downloadID)) {
final completer = Completer<File?>();
_fileDownloadsInProgress[downloadID] = completer.future;
Future<File?> downloadFuture;
downloadFuture = _downloadAndCache(
file,
cacheManager,
progressCallback: (count, total) {
_progressCallbacks[downloadID]?.call(count, total);
},
);
// ignore: unawaited_futures
downloadFuture.then((downloadedFile) async {
completer.complete(downloadedFile);
await _fileDownloadsInProgress.remove(downloadID);
_progressCallbacks.remove(downloadID);
});
}
return _fileDownloadsInProgress[downloadID];
}
Future<File?> getFileFromServer(
EnteFile file, {
ProgressCallback? progressCallback,