Fix download for large video (#1367)

This commit is contained in:
Neeraj Gupta 2023-09-01 13:54:31 +05:30 committed by GitHub
commit c92cd4e17b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 58 deletions

View file

@ -2,10 +2,8 @@ import 'dart:async';
import "dart:io";
import 'package:flutter/material.dart';
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import 'package:photo_manager/photo_manager.dart';
import "package:photos/core/configuration.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/ui/common/gradient_button.dart';

View file

@ -11,6 +11,7 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
@ -307,7 +308,11 @@ class FileAppBarState extends State<FileAppBar> {
}
Future<void> _download(EnteFile file) async {
final dialog = createProgressDialog(context, "Downloading...");
final dialog = createProgressDialog(
context,
context.l10n.downloading,
isDismissible: true,
);
await dialog.show();
try {
final FileType type = file.fileType;

View file

@ -92,6 +92,9 @@ class _VideoWidgetState extends State<VideoWidget> {
getFileFromServer(
widget.file,
progressCallback: (count, total) {
if(!mounted) {
return;
}
_progressNotifier.value = count / (widget.file.fileSize ?? total);
if (_progressNotifier.value == 1) {
if (mounted) {
@ -100,18 +103,24 @@ class _VideoWidgetState extends State<VideoWidget> {
}
},
).then((file) {
if (file != null) {
if (file != null && mounted) {
_setVideoPlayerController(file: file);
}
}).onError((error, stackTrace) {
showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo);
if(mounted) {
showErrorDialog(context, "Error", S
.of(context).failedToDownloadVideo,);
}
});
}
@override
void dispose() {
removeCallBack(widget.file);
_videoPlayerController?.dispose();
_chewieController?.dispose();
_progressNotifier.dispose();
if (_wakeLockEnabledHere) {
unawaited(
WakelockPlus.enabled.then((isEnabled) {

View file

@ -0,0 +1,44 @@
import 'dart:async';
import "package:flutter/foundation.dart";
typedef FakeProgressCallback = void Function(int count);
class FakePeriodicProgress {
final FakeProgressCallback? callback;
final Duration duration;
late Timer _timer;
bool _shouldRun = true;
int runCount = 0;
FakePeriodicProgress({
required this.callback,
required this.duration,
});
void start() {
assert(_shouldRun, "Cannot start a stopped FakePeriodicProgress");
Future.delayed(duration, _invokePeriodically);
}
void stop() {
if (_shouldRun) {
_shouldRun = false;
_timer.cancel();
}
}
void _invokePeriodically() {
if (_shouldRun) {
try {
runCount++;
callback?.call(runCount);
} catch (e) {
debugPrint("Error in FakePeriodicProgress callback: $e");
stop();
return;
}
_timer = Timer(duration, _invokePeriodically);
}
}
}

View file

@ -1,75 +1,97 @@
import 'dart:io';
import 'dart:typed_data';
import "package:computer/computer.dart";
import 'package:dio/dio.dart';
import "package:flutter/foundation.dart";
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/network/network.dart';
import 'package:photos/models/file/file.dart';
import "package:photos/models/file/file_type.dart";
import 'package:photos/services/collections_service.dart';
import 'package:photos/utils/crypto_util.dart';
import "package:photos/utils/data_util.dart";
import "package:photos/utils/fake_progress.dart";
final _logger = Logger("file_download_util");
Future<File?> downloadAndDecrypt(
EnteFile file, {
ProgressCallback? progressCallback,
}) {
}) async {
final String logPrefix = 'File-${file.uploadedFileID}:';
_logger.info('$logPrefix starting download');
final encryptedFilePath = Configuration.instance.getTempDirectory() +
file.generatedID.toString() +
".encrypted";
_logger
.info('$logPrefix starting download ${formatBytes(file.fileSize ?? 0)}');
final String tempDir = Configuration.instance.getTempDirectory();
final String encryptedFilePath = "$tempDir${file.generatedID}.encrypted";
final encryptedFile = File(encryptedFilePath);
final startTime = DateTime.now().millisecondsSinceEpoch;
return NetworkClient.instance
.getDio()
.download(
file.downloadUrl,
encryptedFilePath,
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
onReceiveProgress: progressCallback,
)
.then((response) async {
if (response.statusCode != 200) {
_logger.warning('$logPrefix download failed ${response.toString()}');
return null;
} else if (!encryptedFile.existsSync()) {
_logger.warning('$logPrefix incomplete download, file not found');
try {
final response = await NetworkClient.instance.getDio().download(
file.downloadUrl,
encryptedFilePath,
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
onReceiveProgress: (a, b) {
if (kDebugMode) {
_logger.fine(
"$logPrefix download progress: ${formatBytes(a)} / ${formatBytes(b)}",
);
}
progressCallback?.call(a, b);
},
);
if (response.statusCode != 200 || !encryptedFile.existsSync()) {
_logger.warning('$logPrefix download failed ${response.toString()}');
return null;
}
final int sizeInBytes = ((file.fileSize ?? 0) > 0)
? file.fileSize!
: await encryptedFile.length();
final double speedInKBps = sizeInBytes /
1024.0 /
((DateTime.now().millisecondsSinceEpoch - startTime) / 1000);
final int sizeInBytes = file.fileSize ?? await encryptedFile.length();
final double elapsedSeconds =
(DateTime.now().millisecondsSinceEpoch - startTime) / 1000;
final double speedInKBps = sizeInBytes / 1024.0 / elapsedSeconds;
_logger.info(
"$logPrefix download completed: ${formatBytes(sizeInBytes)}, avg speed: ${speedInKBps.toStringAsFixed(2)} KB/s",
'$logPrefix download completed: ${formatBytes(sizeInBytes)}, avg speed: ${speedInKBps.toStringAsFixed(2)} KB/s',
);
final decryptedFilePath = Configuration.instance.getTempDirectory() +
file.generatedID.toString() +
".decrypted";
final String decryptedFilePath = "$tempDir${file.generatedID}.decrypted";
// As decryption can take time, emit fake progress for large files during
// decryption
final FakePeriodicProgress? fakeProgress = file.fileType == FileType.video
? FakePeriodicProgress(
callback: (count) {
progressCallback?.call(sizeInBytes, sizeInBytes);
},
duration: const Duration(milliseconds: 5000),
)
: null;
try {
// Start the periodic callback after initial 5 seconds
fakeProgress?.start();
await CryptoUtil.decryptFile(
encryptedFilePath,
decryptedFilePath,
CryptoUtil.base642bin(file.fileDecryptionHeader!),
getFileKey(file),
);
fakeProgress?.stop();
_logger.info('$logPrefix decryption completed');
} catch (e, s) {
_logger.severe("failed to decrypt file", e, s);
fakeProgress?.stop();
_logger.severe("Critical: $logPrefix failed to decrypt", e, s);
return null;
}
_logger.info('$logPrefix decryption completed');
await encryptedFile.delete();
return File(decryptedFilePath);
});
} catch (e, s) {
_logger.severe("$logPrefix failed to download or decrypt", e, s);
return null;
}
}
Uint8List getFileKey(EnteFile file) {

View file

@ -14,6 +14,7 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
import 'package:photos/core/cache/video_cache_manager.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import 'package:photos/utils/file_download_util.dart';
@ -108,8 +109,21 @@ void preloadThumbnail(EnteFile file) {
}
}
final Map<String, Future<File?>> fileDownloadsInProgress =
final Map<String, Future<File?>> _fileDownloadsInProgress =
<String, Future<File?>>{};
Map<String, ProgressCallback?> _progressCallbacks = {};
void removeCallBack(EnteFile file) {
if (!file.isUploaded) {
return;
}
String id = file.uploadedFileID.toString() + false.toString();
_progressCallbacks.remove(id);
if (file.isLivePhoto) {
id = file.uploadedFileID.toString() + true.toString();
_progressCallbacks.remove(id);
}
}
Future<File?> getFileFromServer(
EnteFile file, {
@ -124,30 +138,44 @@ Future<File?> getFileFromServer(
return fileFromCache.file;
}
final downloadID = file.uploadedFileID.toString() + liveVideo.toString();
if (!fileDownloadsInProgress.containsKey(downloadID)) {
if (progressCallback != null) {
_progressCallbacks[downloadID] = progressCallback;
}
if (!_fileDownloadsInProgress.containsKey(downloadID)) {
final completer = Completer<File?>();
_fileDownloadsInProgress[downloadID] = completer.future;
Future<File?> downloadFuture;
if (file.fileType == FileType.livePhoto) {
fileDownloadsInProgress[downloadID] = _getLivePhotoFromServer(
downloadFuture = _getLivePhotoFromServer(
file,
progressCallback: progressCallback,
progressCallback: (count, total) {
_progressCallbacks[downloadID]?.call(count, total);
},
needLiveVideo: liveVideo,
);
fileDownloadsInProgress[downloadID]!.whenComplete(() {
fileDownloadsInProgress.remove(downloadID);
});
} else {
fileDownloadsInProgress[downloadID] = _downloadAndCache(
downloadFuture = _downloadAndCache(
file,
cacheManager,
progressCallback: progressCallback,
progressCallback: (count, total) {
_progressCallbacks[downloadID]?.call(count, total);
},
);
fileDownloadsInProgress[downloadID]!.whenComplete(() {
fileDownloadsInProgress.remove(downloadID);
});
}
downloadFuture.then((downloadedFile) {
completer.complete(downloadedFile);
_fileDownloadsInProgress.remove(downloadID);
_progressCallbacks.remove(downloadID);
});
}
return fileDownloadsInProgress[downloadID];
return _fileDownloadsInProgress[downloadID];
}
Future<bool> isFileCached(EnteFile file, {bool liveVideo = false}) async {
final cacheManager = (file.fileType == FileType.video || liveVideo)
? VideoCacheManager.instance
@ -237,9 +265,9 @@ Future<_LivePhoto?> _downloadLivePhoto(
final videoFile = File(decodePath);
await videoFile.create(recursive: true);
await videoFile.writeAsBytes(data);
videoFileCache = await VideoCacheManager.instance.putFile(
videoFileCache = await VideoCacheManager.instance.putFileStream(
file.downloadUrl,
await videoFile.readAsBytes(),
videoFile.openRead(),
eTag: file.downloadUrl,
maxAge: const Duration(days: 365),
fileExtension: fileExtension,
@ -263,7 +291,7 @@ Future<_LivePhoto?> _downloadLivePhoto(
Future<File?> _downloadAndCache(
EnteFile file,
BaseCacheManager cacheManager, {
ProgressCallback? progressCallback,
required ProgressCallback progressCallback,
}) async {
return downloadAndDecrypt(file, progressCallback: progressCallback)
.then((decryptedFile) async {
@ -287,9 +315,9 @@ Future<File?> _downloadAndCache(
}
await decryptedFile.delete();
}
final cachedFile = await cacheManager.putFile(
final cachedFile = await cacheManager.putFileStream(
file.downloadUrl,
await outputFile.readAsBytes(),
outputFile.openRead(),
eTag: file.downloadUrl,
maxAge: const Duration(days: 365),
fileExtension: fileExtension,