fix: only show when video streaming is enabled (#7031)

## Description

## Tests
This commit is contained in:
Neeraj
2025-09-02 10:45:37 +05:30
committed by GitHub
7 changed files with 135 additions and 52 deletions

View File

@@ -181,6 +181,8 @@ PODS:
- PromisesObjC (2.4.0)
- receive_sharing_intent (1.8.1):
- Flutter
- rive_common (0.0.1):
- Flutter
- rust_lib_photos (0.0.1):
- Flutter
- SDWebImage (5.21.1):
@@ -291,6 +293,7 @@ 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`)
- rive_common (from `.symlinks/plugins/rive_common/ios`)
- rust_lib_photos (from `.symlinks/plugins/rust_lib_photos/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -418,6 +421,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/privacy_screen/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
rive_common:
:path: ".symlinks/plugins/rive_common/ios"
rust_lib_photos:
:path: ".symlinks/plugins/rust_lib_photos/ios"
sentry_flutter:
@@ -510,6 +515,7 @@ SPEC CHECKSUMS:
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
rust_lib_photos: 04d3901908d2972192944083310b65abf410774c
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380

View File

@@ -565,6 +565,7 @@
"${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}/rive_common/rive_common.framework",
"${BUILT_PRODUCTS_DIR}/rust_lib_photos/rust_lib_photos.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
@@ -662,6 +663,7 @@
"${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}/rive_common.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/rust_lib_photos.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",

View File

@@ -49,32 +49,47 @@ class ComputeController {
ComputeController() {
_logger.info('ComputeController constructor');
init();
_logger.info('init done ');
}
// Directly assign the values + Attach listener for compute controller
Future<void> init() async {
// Interaction Timer
_startInteractionTimer(kDefaultInteractionTimeout);
// Thermal related
_onThermalStateUpdate(await _thermal.thermalStatus);
_thermal.onThermalStatusChanged.listen((ThermalStatus thermalState) {
_onThermalStateUpdate(thermalState);
});
// Battery State
if (Platform.isIOS) {
if (kDebugMode) {
_logger.info(
_logger.fine(
"iOS battery info stream is not available in simulator, disabling in debug mode",
);
// if you need to test on physical device, uncomment this check
return;
} else {
// Update Battery state for iOS
_oniOSBatteryStateUpdate(await BatteryInfoPlugin().iosBatteryInfo);
BatteryInfoPlugin()
.iosBatteryInfoStream
.listen((IosBatteryInfo? batteryInfo) {
_oniOSBatteryStateUpdate(batteryInfo);
});
}
BatteryInfoPlugin()
.iosBatteryInfoStream
.listen((IosBatteryInfo? batteryInfo) {
_oniOSBatteryStateUpdate(batteryInfo);
});
}
if (Platform.isAndroid) {
} else if (Platform.isAndroid) {
// Update Battery state for Android
_onAndroidBatteryStateUpdate(
await BatteryInfoPlugin().androidBatteryInfo,
);
BatteryInfoPlugin()
.androidBatteryInfoStream
.listen((AndroidBatteryInfo? batteryInfo) {
_onAndroidBatteryStateUpdate(batteryInfo);
});
}
_thermal.onThermalStatusChanged.listen((ThermalStatus thermalState) {
_onThermalStateUpdate(thermalState);
});
_logger.info('init done ');
}
bool requestCompute({

View File

@@ -157,7 +157,25 @@ class VideoPreviewService {
bool isCurrentlyProcessing(int? uploadedFileID) {
if (uploadedFileID == null) return false;
return uploadingFileId == uploadedFileID;
// Also check if file is in queue or other processing states
final item = _items[uploadedFileID];
if (item != null) {
switch (item.status) {
case PreviewItemStatus.inQueue:
case PreviewItemStatus.compressing:
case PreviewItemStatus.uploading:
return true;
default:
return false;
}
}
return false;
}
PreviewItemStatus? getProcessingStatus(int uploadedFileID) {
return _items[uploadedFileID]?.status;
}
Future<bool> _isRecreateOperation(EnteFile file) async {
@@ -256,15 +274,20 @@ class VideoPreviewService {
Future<void> chunkAndUploadVideo(
BuildContext? ctx,
EnteFile enteFile, [
EnteFile enteFile, {
/// Indicates this function is an continuation of a chunking thread
bool continuation = false,
// not used currently
bool forceUpload = false,
bool isManual = false,
]) async {
}) async {
final bool isManual =
await uploadLocksDB.isInStreamQueue(enteFile.uploadedFileID!);
final canStream = _isPermissionGranted();
if (!canStream) {
_logger.info(
"Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission) - isManual: $isManual",
);
computeController.releaseCompute(stream: true);
if (isVideoStreamingEnabled) _logger.info("No permission to run compute");
clearQueue();
return;
@@ -311,7 +334,7 @@ class VideoPreviewService {
}
// check if there is already a preview in processing
if (uploadingFileId >= 0) {
if (!continuation && uploadingFileId >= 0) {
if (uploadingFileId == enteFile.uploadedFileID) return;
_items[enteFile.uploadedFileID!] = PreviewItem(
@@ -562,21 +585,26 @@ class VideoPreviewService {
_removeFile(enteFile);
_removeFromLocks(enteFile).ignore();
}
// reset uploading status if this was getting processed
if (uploadingFileId == enteFile.uploadedFileID!) {
uploadingFileId = -1;
}
_logger.info(
"[chunk] Processing ${_items.length} items for streaming, $error",
);
// process next file
if (fileQueue.isNotEmpty) {
// process next file
_logger.info(
"[chunk] Processing ${_items.length} items for streaming, $error",
);
final entry = fileQueue.entries.first;
final file = entry.value;
fileQueue.remove(entry.key);
await chunkAndUploadVideo(ctx, file);
await chunkAndUploadVideo(
ctx,
file,
continuation: true,
);
} else {
_logger.info(
"[chunk] Nothing to process releasing compute, $error",
);
computeController.releaseCompute(stream: true);
uploadingFileId = -1;
}
}
}
@@ -987,8 +1015,9 @@ class VideoPreviewService {
}
// generate stream for all files after cutoff date
Future<void> _putFilesForPreviewCreation() async {
if (!isVideoStreamingEnabled || !await canUseHighBandwidth()) return;
// returns false if it fails to launch chuncking function
Future<bool> _putFilesForPreviewCreation() async {
if (!isVideoStreamingEnabled || !await canUseHighBandwidth()) return false;
Map<int, String> failureFiles = {};
Map<int, String> manualQueueFiles = {};
@@ -1124,7 +1153,7 @@ class VideoPreviewService {
final totalFiles = fileQueue.length;
if (totalFiles == 0) {
_logger.info("[init] No preview to cache");
return;
return false;
}
_logger.info(
@@ -1136,6 +1165,7 @@ class VideoPreviewService {
final file = entry.value;
fileQueue.remove(entry.key);
chunkAndUploadVideo(null, file).ignore();
return true;
}
bool _allowStream() {
@@ -1152,9 +1182,11 @@ class VideoPreviewService {
);
}
/// To check if it's enabled, device is healthy and running streaming
bool _isPermissionGranted() {
return isVideoStreamingEnabled &&
computeController.computeState == ComputeRunState.generatingStream;
computeController.computeState == ComputeRunState.generatingStream &&
computeController.isDeviceHealthy;
}
void queueFiles({
@@ -1169,7 +1201,11 @@ class VideoPreviewService {
if (!isStreamAllowed) return;
await _ensurePreviewIdsInitialized();
await _putFilesForPreviewCreation();
final result = await _putFilesForPreviewCreation();
// Cannot proceed to stream generation, would have to release compute ASAP
if (!result) {
computeController.releaseCompute(stream: true);
}
});
}
}

View File

@@ -499,7 +499,8 @@ class FileAppBarState extends State<FileAppBar> {
widget.file.isUploaded &&
widget.file.fileSize != null &&
(widget.file.pubMagicMetadata?.sv ?? 0) != 1 &&
widget.file.ownerID == userId;
widget.file.ownerID == userId &&
VideoPreviewService.instance.isVideoStreamingEnabled;
}
Future<void> _handleVideoStream(String streamType) async {

View File

@@ -7,8 +7,8 @@ import 'package:logging/logging.dart';
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/exceptions.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/core/exceptions.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/db/trash_db.dart';
import 'package:photos/events/files_updated_event.dart';
@@ -332,7 +332,9 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
//Do not retry if the widget is not mounted
if (!mounted) {
throw WidgetUnmountedException("Thumbnail loading cancelled: widget unmounted");
throw WidgetUnmountedException(
"Thumbnail loading cancelled: widget unmounted",
);
}
retryAttempts++;

View File

@@ -32,27 +32,34 @@ class VideoStreamChangeWidget extends StatefulWidget {
class _VideoStreamChangeWidgetState extends State<VideoStreamChangeWidget> {
StreamSubscription<VideoPreviewStateChangedEvent>? _subscription;
bool isCurrentlyProcessing = false;
@override
void initState() {
super.initState();
// Initialize processing state safely in initState
isCurrentlyProcessing = VideoPreviewService.instance
.isCurrentlyProcessing(widget.file.uploadedFileID);
_subscription =
Bus.instance.on<VideoPreviewStateChangedEvent>().listen((event) {
final fileId = event.fileId;
if (widget.file.uploadedFileID != fileId) {
return; // Not for this file
}
final status = event.status;
// Handle different states
switch (status) {
case PreviewItemStatus.inQueue:
case PreviewItemStatus.uploaded:
case PreviewItemStatus.failed:
setState(() {});
break;
default:
// Handle different states - will be false for different files or non-processing states
final newProcessingState = widget.file.uploadedFileID == fileId && switch (status) {
PreviewItemStatus.inQueue ||
PreviewItemStatus.retry ||
PreviewItemStatus.compressing ||
PreviewItemStatus.uploading =>
true,
_ => false,
};
// Only update state if value changed
if (isCurrentlyProcessing != newProcessingState) {
isCurrentlyProcessing = newProcessingState;
setState(() {});
}
});
}
@@ -63,14 +70,28 @@ class _VideoStreamChangeWidgetState extends State<VideoStreamChangeWidget> {
super.dispose();
}
String _getStatusText(BuildContext context, PreviewItemStatus? status) {
switch (status) {
case PreviewItemStatus.inQueue:
case PreviewItemStatus.retry:
return AppLocalizations.of(context).queued;
case PreviewItemStatus.compressing:
case PreviewItemStatus.uploading:
default:
return AppLocalizations.of(context).creatingStream;
}
}
@override
Widget build(BuildContext context) {
final bool isPreviewAvailable = widget.file.uploadedFileID != null &&
(fileDataService.previewIds.containsKey(widget.file.uploadedFileID));
// Check if this file is currently being processed for streaming
final bool isCurrentlyProcessing = VideoPreviewService.instance
.isCurrentlyProcessing(widget.file.uploadedFileID);
// Get the current processing status for more specific messaging
final processingStatus = widget.file.uploadedFileID != null
? VideoPreviewService.instance
.getProcessingStatus(widget.file.uploadedFileID!)
: null;
final colorScheme = getEnteColorScheme(context);
@@ -125,7 +146,7 @@ class _VideoStreamChangeWidgetState extends State<VideoStreamChangeWidget> {
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context).creatingStream,
_getStatusText(context, processingStatus),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,