diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index a379bebe6f..454be3399a 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1831,7 +1831,8 @@ "videosProcessed": "Videos processed", "totalVideos": "Total videos", "skippedVideos": "Skipped videos", - "videoStreamingDescription": "Play videos instantly on any device.\nEnable to process video streams on this device.", + "videoStreamingDescriptionLine1": "Play videos instantly on any device.", + "videoStreamingDescriptionLine2": "Enable to process video streams on this device.", "videoStreamingNote": "Only videos from last 60 days and under 1 minute are processed on this device. For older/longer videos, enable streaming in the desktop app.", "createStream": "Create stream", "recreateStream": "Recreate stream", diff --git a/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart b/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart index 81a0d91b4d..32ff15e352 100644 --- a/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart +++ b/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart @@ -10,7 +10,7 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/compute_control_event.dart"; import "package:thermal/thermal.dart"; -enum _ComputeRunState { +enum ComputeRunState { idle, runningML, generatingStream, @@ -35,7 +35,7 @@ class ComputeController { bool interactionOverride = false; late Timer _userInteractionTimer; - _ComputeRunState _currentRunState = _ComputeRunState.idle; + ComputeRunState _currentRunState = ComputeRunState.idle; bool _waitingToRunML = false; bool get isDeviceHealthy => _isDeviceHealthy; @@ -70,10 +70,20 @@ class ComputeController { _logger.info('init done '); } - bool requestCompute({bool ml = false, bool stream = false}) { - _logger.info("Requesting compute: ml: $ml, stream: $stream"); - if (!_isDeviceHealthy || !_canRunGivenUserInteraction()) { - _logger.info("Device not healthy or user interacting, denying request."); + bool requestCompute({ + bool ml = false, + bool stream = false, + bool bypassInteractionCheck = false, + }) { + _logger.info( + "Requesting compute: ml: $ml, stream: $stream, bypassInteraction: $bypassInteractionCheck", + ); + if (!_isDeviceHealthy) { + _logger.info("Device not healthy, denying request."); + return false; + } + if (!bypassInteractionCheck && !_canRunGivenUserInteraction()) { + _logger.info("User interacting, denying request."); return false; } bool result = false; @@ -87,13 +97,17 @@ class ComputeController { return result; } + ComputeRunState get computeState { + return _currentRunState; + } + bool _requestML() { - if (_currentRunState == _ComputeRunState.idle) { - _currentRunState = _ComputeRunState.runningML; + if (_currentRunState == ComputeRunState.idle) { + _currentRunState = ComputeRunState.runningML; _waitingToRunML = false; _logger.info("ML request granted"); return true; - } else if (_currentRunState == _ComputeRunState.runningML) { + } else if (_currentRunState == ComputeRunState.runningML) { return true; } _logger.info( @@ -104,12 +118,9 @@ class ComputeController { } bool _requestStream() { - if (_currentRunState == _ComputeRunState.idle && !_waitingToRunML) { + if (_currentRunState == ComputeRunState.idle && !_waitingToRunML) { _logger.info("Stream request granted"); - _currentRunState = _ComputeRunState.generatingStream; - return true; - } else if (_currentRunState == _ComputeRunState.generatingStream && - !_waitingToRunML) { + _currentRunState = ComputeRunState.generatingStream; return true; } _logger.info( @@ -124,13 +135,13 @@ class ComputeController { ); if (ml) { - if (_currentRunState == _ComputeRunState.runningML) { - _currentRunState = _ComputeRunState.idle; + if (_currentRunState == ComputeRunState.runningML) { + _currentRunState = ComputeRunState.idle; } _waitingToRunML = false; } else if (stream) { - if (_currentRunState == _ComputeRunState.generatingStream) { - _currentRunState = _ComputeRunState.idle; + if (_currentRunState == ComputeRunState.generatingStream) { + _currentRunState = ComputeRunState.idle; } } } diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index c7d3abd982..49f47f4e81 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -32,6 +32,7 @@ import "package:photos/service_locator.dart"; import "package:photos/services/file_magic_service.dart"; import "package:photos/services/filedata/model/file_data.dart"; import "package:photos/services/isolated_ffmpeg_service.dart"; +import "package:photos/services/machine_learning/compute_controller.dart"; import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_key.dart"; @@ -120,8 +121,9 @@ class VideoPreviewService { if (file.uploadedFileID == null) return false; // Check if already in queue - final bool alreadyInQueue = - await uploadLocksDB.isInStreamQueue(file.uploadedFileID!); + final bool alreadyInQueue = await uploadLocksDB.isInStreamQueue( + file.uploadedFileID!, + ); if (alreadyInQueue) { return false; // Indicates file was already in queue } @@ -131,7 +133,7 @@ class VideoPreviewService { // Start processing if not already processing if (uploadingFileId < 0) { - queueFiles(duration: Duration.zero); + queueFiles(duration: Duration.zero, isManual: true); } else { _items[file.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.inQueue, @@ -252,10 +254,12 @@ class VideoPreviewService { BuildContext? ctx, EnteFile enteFile, [ bool forceUpload = false, + bool isManual = false, ]) async { - if (!_allowStream()) { + final canStream = _isPermissionGranted(); + if (!canStream) { _logger.info( - "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission)", + "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission) - isManual: $isManual", ); if (isVideoStreamingEnabled) _logger.info("No permission to run compute"); clearQueue(); @@ -295,7 +299,8 @@ class VideoPreviewService { "Starting video preview generation for ${enteFile.displayName}", ); // elimination case for <=10 MB with H.264 - var (props, result, file) = await _checkFileForPreviewCreation(enteFile); + var (props, result, file) = + await _checkFileForPreviewCreation(enteFile, isManual); if (result) { removeFile = true; return; @@ -575,8 +580,9 @@ class VideoPreviewService { Future _removeFromLocks(EnteFile enteFile) async { final bool isFailurePresent = _failureFiles?.contains(enteFile.uploadedFileID!) ?? false; - final bool isInManualQueue = - await uploadLocksDB.isInStreamQueue(enteFile.uploadedFileID!); + final bool isInManualQueue = await uploadLocksDB.isInStreamQueue( + enteFile.uploadedFileID!, + ); if (isFailurePresent) { await uploadLocksDB.deleteStreamUploadErrorEntry( @@ -917,27 +923,35 @@ class VideoPreviewService { } Future<(FFProbeProps?, bool, File?)> _checkFileForPreviewCreation( - EnteFile enteFile, - ) async { + EnteFile enteFile, [ + bool isManual = false, + ]) async { if ((enteFile.pubMagicMetadata?.sv ?? 0) == 1) { _logger.info("Skip Preview due to sv=1 for ${enteFile.displayName}"); return (null, true, null); } - if (enteFile.fileSize == null || enteFile.duration == null) { - _logger.warning( - "Skip Preview due to misisng size/duration for ${enteFile.displayName}", - ); - return (null, true, null); - } - final int size = enteFile.fileSize!; - final int duration = enteFile.duration!; - if (size >= 500 * 1024 * 1024 || duration > 60) { - _logger.info("Skip Preview due to size: $size or duration: $duration"); - return (null, true, null); + if (!isManual) { + if (enteFile.fileSize == null || enteFile.duration == null) { + _logger.warning( + "Skip Preview due to misisng size/duration for ${enteFile.displayName}", + ); + return (null, true, null); + } + final int size = enteFile.fileSize!; + final int duration = enteFile.duration!; + if (size >= 500 * 1024 * 1024 || duration > 60) { + _logger.info("Skip Preview due to size: $size or duration: $duration"); + return (null, true, null); + } } FFProbeProps? props; File? file; bool skipFile = false; + if (enteFile.fileSize == null && isManual) { + return (props, skipFile, file); + } + + final size = enteFile.fileSize ?? 0; try { final isFileUnder10MB = size <= 10 * 1024 * 1024; if (isFileUnder10MB) { @@ -1025,8 +1039,9 @@ class VideoPreviewService { } // First try to find the file in the 60-day list - var queueFile = - files.firstWhereOrNull((f) => f.uploadedFileID == queueFileId); + var queueFile = files.firstWhereOrNull( + (f) => f.uploadedFileID == queueFileId, + ); // If not found in 60-day list, fetch it individually queueFile ??= @@ -1124,11 +1139,27 @@ class VideoPreviewService { computeController.requestCompute(stream: true); } - void queueFiles({Duration duration = const Duration(seconds: 5)}) { + bool _allowManualStream() { + return isVideoStreamingEnabled && + computeController.requestCompute( + stream: true, + bypassInteractionCheck: true, + ); + } + + bool _isPermissionGranted() { + return isVideoStreamingEnabled && + computeController.computeState == ComputeRunState.generatingStream; + } + + void queueFiles({ + Duration duration = const Duration(seconds: 5), + bool isManual = false, + }) { Future.delayed(duration, () async { if (_hasQueuedFile) return; - final isStreamAllowed = _allowStream(); + final isStreamAllowed = isManual ? _allowManualStream() : _allowStream(); if (!isStreamAllowed) return; await _ensurePreviewIdsInitialized(); diff --git a/mobile/apps/photos/lib/ui/settings/streaming/video_streaming_settings_page.dart b/mobile/apps/photos/lib/ui/settings/streaming/video_streaming_settings_page.dart index 0f549dedba..dc8d2a8476 100644 --- a/mobile/apps/photos/lib/ui/settings/streaming/video_streaming_settings_page.dart +++ b/mobile/apps/photos/lib/ui/settings/streaming/video_streaming_settings_page.dart @@ -100,7 +100,12 @@ class _VideoStreamingSettingsPageState children: [ TextSpan( text: AppLocalizations.of(context) - .videoStreamingDescription, + .videoStreamingDescriptionLine1, + ), + const TextSpan(text: " "), + TextSpan( + text: AppLocalizations.of(context) + .videoStreamingDescriptionLine2, ), const TextSpan(text: " "), TextSpan( @@ -113,7 +118,6 @@ class _VideoStreamingSettingsPageState ), ], ), - textAlign: TextAlign.justify, style: getEnteTextTheme(context).mini.copyWith( color: getEnteColorScheme(context).textMuted, ), @@ -145,10 +149,17 @@ class _VideoStreamingSettingsPageState padding: const EdgeInsets.symmetric(horizontal: 12), child: Text.rich( TextSpan( - text: AppLocalizations.of(context) - .videoStreamingDescription + - "\n", children: [ + TextSpan( + text: AppLocalizations.of(context) + .videoStreamingDescriptionLine1, + ), + const TextSpan(text: "\n"), + TextSpan( + text: AppLocalizations.of(context) + .videoStreamingDescriptionLine2, + ), + const TextSpan(text: "\n"), TextSpan( text: AppLocalizations.of(context).moreDetails, style: TextStyle( diff --git a/mobile/apps/photos/lib/ui/viewer/file/file_app_bar.dart b/mobile/apps/photos/lib/ui/viewer/file/file_app_bar.dart index 27cbe4e9f4..3701424e27 100644 --- a/mobile/apps/photos/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/apps/photos/lib/ui/viewer/file/file_app_bar.dart @@ -497,6 +497,7 @@ class FileAppBarState extends State { final userId = Configuration.instance.getUserID(); return widget.file.fileType == FileType.video && widget.file.isUploaded && + widget.file.fileSize != null && (widget.file.pubMagicMetadata?.sv ?? 0) != 1 && widget.file.ownerID == userId; } diff --git a/mobile/apps/photos/scripts/internal_changes.txt b/mobile/apps/photos/scripts/internal_changes.txt index 31a86b8fc0..2e884239d0 100644 --- a/mobile/apps/photos/scripts/internal_changes.txt +++ b/mobile/apps/photos/scripts/internal_changes.txt @@ -1,4 +1,7 @@ - Similar images design changes. Also changed the vectorDB index file name, so internal users will have another migration (long loading time). +- Prateek: Enable immediate manual video stream processing by bypassing user interaction timer +- Prateek: Fix multiple concurrent streaming processes bug in ComputeController +- Prateek: Fix video streaming description text display spacing in advanced settings - Ashil: Render cached thumbnails faster (noticeable in gallery scrolling) - Similar images UI changes - Neeraj: Fix for double enteries for local file diff --git a/mobile/apps/photos/scripts/store_changes.txt b/mobile/apps/photos/scripts/store_changes.txt index f1baf26cae..d7bc0e0206 100644 --- a/mobile/apps/photos/scripts/store_changes.txt +++ b/mobile/apps/photos/scripts/store_changes.txt @@ -1,3 +1,4 @@ +- Video streaming improvements - Added support for custom domain links - Image editor fixes: - Fixed bottom navigation bar color in light theme