From ff37c4bf81968c623be51a0c11682c9c95adafa2 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 11:44:28 +0000 Subject: [PATCH 01/10] fix: video streaming description spacing and alignment - Split videoStreamingDescription into separate line1/line2 localization keys - Remove TextAlign.justify from enabled state to fix awkward word spacing - Standardize text rendering between enabled and disabled states - Both states now display description consistently without spacing issues Co-authored-by: Claude --- mobile/apps/photos/lib/l10n/intl_en.arb | 3 ++- .../video_streaming_settings_page.dart | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index 8bbf7a593d..b937e4df58 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/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( From ecca4c3dc8d87a658d01d3e792f203d7ab336143 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 13:00:21 +0000 Subject: [PATCH 02/10] feat: bypass interaction check for manual stream requests Co-authored-by: Claude --- .../machine_learning/compute_controller.dart | 12 ++++++++---- .../lib/services/video_preview_service.dart | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) 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..90ad9401ed 100644 --- a/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart +++ b/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart @@ -70,10 +70,14 @@ 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; diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index c7d3abd982..5b7113f3dd 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -131,7 +131,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,13 +252,15 @@ class VideoPreviewService { BuildContext? ctx, EnteFile enteFile, [ bool forceUpload = false, + bool isManual = false, ]) async { - if (!_allowStream()) { + final canStream = isManual ? _allowManualStream() : _allowStream(); + 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(); + if (!isManual) clearQueue(); // Only clear queue for automatic processing return; } @@ -1124,11 +1126,16 @@ class VideoPreviewService { computeController.requestCompute(stream: true); } - void queueFiles({Duration duration = const Duration(seconds: 5)}) { + bool _allowManualStream() { + return isVideoStreamingEnabled && + computeController.requestCompute(stream: true, bypassInteractionCheck: true); + } + + 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(); From ad3901d4843476f3349596850f3ec61047a1b0a3 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 13:08:12 +0000 Subject: [PATCH 03/10] fix: remove conditional clearQueue for manual processing Co-authored-by: Claude --- mobile/apps/photos/lib/services/video_preview_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index 5b7113f3dd..74cd868473 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -260,7 +260,7 @@ class VideoPreviewService { "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission) - isManual: $isManual", ); if (isVideoStreamingEnabled) _logger.info("No permission to run compute"); - if (!isManual) clearQueue(); // Only clear queue for automatic processing + clearQueue(); return; } From e9554ffbcb5518d65409586baeb4142403f739d0 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 13:15:10 +0000 Subject: [PATCH 04/10] fix: prevent multiple concurrent streaming processes Remove condition allowing additional stream requests when already streaming to ensure only one stream process runs at a time. Co-authored-by: Claude --- .../lib/services/machine_learning/compute_controller.dart | 3 --- 1 file changed, 3 deletions(-) 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 90ad9401ed..14700efef8 100644 --- a/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart +++ b/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart @@ -112,9 +112,6 @@ class ComputeController { _logger.info("Stream request granted"); _currentRunState = _ComputeRunState.generatingStream; return true; - } else if (_currentRunState == _ComputeRunState.generatingStream && - !_waitingToRunML) { - return true; } _logger.info( "Stream request denied, current state: $_currentRunState, wants to run ML: $_waitingToRunML", From 4484b9e4ada7c61c7e24fd385e611d5e916cd74f Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 13:21:59 +0000 Subject: [PATCH 05/10] update: add video streaming improvements to change logs Co-authored-by: Claude --- mobile/apps/photos/scripts/internal_changes.txt | 3 +++ mobile/apps/photos/scripts/store_changes.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/mobile/apps/photos/scripts/internal_changes.txt b/mobile/apps/photos/scripts/internal_changes.txt index 286f24adeb..5b09ac5e0f 100644 --- a/mobile/apps/photos/scripts/internal_changes.txt +++ b/mobile/apps/photos/scripts/internal_changes.txt @@ -1,3 +1,6 @@ +- 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 From ff3864a09a648a77877ee3a9ac572213b81b1f41 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 21:09:10 +0530 Subject: [PATCH 06/10] fix: check only if permission granted before chunking --- .../machine_learning/compute_controller.dart | 34 +++-- .../lib/services/video_preview_service.dart | 140 ++++++++++-------- 2 files changed, 100 insertions(+), 74 deletions(-) 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 14700efef8..9536bf9636 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,8 +70,12 @@ class ComputeController { _logger.info('init done '); } - bool requestCompute({bool ml = false, bool stream = false, bool bypassInteractionCheck = false}) { - _logger.info("Requesting compute: ml: $ml, stream: $stream, bypassInteraction: $bypassInteractionCheck"); + 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; @@ -91,13 +95,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( @@ -108,9 +116,9 @@ class ComputeController { } bool _requestStream() { - if (_currentRunState == _ComputeRunState.idle && !_waitingToRunML) { + if (_currentRunState == ComputeRunState.idle && !_waitingToRunML) { _logger.info("Stream request granted"); - _currentRunState = _ComputeRunState.generatingStream; + _currentRunState = ComputeRunState.generatingStream; return true; } _logger.info( @@ -125,13 +133,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 74cd868473..f4c229f594 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"; @@ -51,14 +52,14 @@ class VideoPreviewService { bool get _hasQueuedFile => fileQueue.isNotEmpty; VideoPreviewService._privateConstructor() - : serviceLocator = ServiceLocator.instance, - filesDB = FilesDB.instance, - uploadLocksDB = UploadLocksDB.instance, - ffmpegService = IsolatedFfmpegService.instance, - fileMagicService = FileMagicService.instance, - cacheManager = DefaultCacheManager(), - videoCacheManager = VideoCacheManager.instance, - config = Configuration.instance; + : serviceLocator = ServiceLocator.instance, + filesDB = FilesDB.instance, + uploadLocksDB = UploadLocksDB.instance, + ffmpegService = IsolatedFfmpegService.instance, + fileMagicService = FileMagicService.instance, + cacheManager = DefaultCacheManager(), + videoCacheManager = VideoCacheManager.instance, + config = Configuration.instance; VideoPreviewService( this.config, @@ -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 } @@ -221,8 +223,9 @@ class VideoPreviewService { // If total is empty then mark all as processed else compute the ratio // of processed files and total remote video files // netProcessedItems = processed / total - final double netProcessedItems = - total.isEmpty ? 1 : (processed.length / total.length).clamp(0, 1); + final double netProcessedItems = total.isEmpty + ? 1 + : (processed.length / total.length).clamp(0, 1); // Store the data and return it final status = netProcessedItems; @@ -254,7 +257,7 @@ class VideoPreviewService { bool forceUpload = false, bool isManual = false, ]) async { - final canStream = isManual ? _allowManualStream() : _allowStream(); + final canStream = _isPermissionGranted(); if (!canStream) { _logger.info( "Pause preview due to disabledSteaming($isVideoStreamingEnabled) or computeController permission) - isManual: $isManual", @@ -328,8 +331,9 @@ class VideoPreviewService { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.compressing, file: enteFile, - retryCount: - forceUpload ? 0 : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, + retryCount: forceUpload + ? 0 + : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); _fireVideoPreviewStateChange( @@ -359,9 +363,11 @@ class VideoPreviewService { ? (fileSize * 8) / props!.duration!.inSeconds : null; - final colorTransfer = - videoData["color_transfer"]?.toString().toLowerCase(); - final isHDR = colorTransfer != null && + final colorTransfer = videoData["color_transfer"] + ?.toString() + .toLowerCase(); + final isHDR = + colorTransfer != null && (colorTransfer == "smpte2084" || colorTransfer == "arib-std-b67"); // create temp file & directory for preview generation @@ -433,17 +439,17 @@ class VideoPreviewService { final playlistGenResult = await ffmpegService .runFfmpeg( - // input file path - '-i "${file.path}" ' + - // main params for streaming - command + - // output file path - '$prefix/output.m3u8', - ) + // input file path + '-i "${file.path}" ' + + // main params for streaming + command + + // output file path + '$prefix/output.m3u8', + ) .onError((error, stackTrace) { - _logger.warning("FFmpeg command failed", error, stackTrace); - return {}; - }); + _logger.warning("FFmpeg command failed", error, stackTrace); + return {}; + }); final playlistGenReturnCode = playlistGenResult["returnCode"] as int?; @@ -475,16 +481,16 @@ class VideoPreviewService { // Fetch resolution of generated stream by decrypting a single frame final playlistFrameResult = await ffmpegService .runFfmpeg( - '-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"', - ) + '-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"', + ) .onError((error, stackTrace) { - _logger.warning( - "FFmpeg command failed for frame", - error, - stackTrace, - ); - return {}; - }); + _logger.warning( + "FFmpeg command failed for frame", + error, + stackTrace, + ); + return {}; + }); final playlistFrameReturnCode = playlistFrameResult["returnCode"] as int?; int? width, height; @@ -577,8 +583,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( @@ -656,16 +663,13 @@ class VideoPreviewService { try { final encryptionKey = getFileKey(file); final playlistContent = playlist.readAsStringSync(); - final result = await gzipAndEncryptJson( - { - "playlist": playlistContent, - 'type': 'hls_video', - 'width': width, - 'height': height, - 'size': objectSize, - }, - encryptionKey, - ); + final result = await gzipAndEncryptJson({ + "playlist": playlistContent, + 'type': 'hls_video', + 'width': width, + 'height': height, + 'size': objectSize, + }, encryptionKey); final _ = await serviceLocator.enteDio.put( "/files/video-data", data: { @@ -784,8 +788,7 @@ class VideoPreviewService { } final videoFile = (await videoCacheManager.getFileFromCache( _getVideoPreviewKey(objectID), - )) - ?.file; + ))?.file; if (videoFile == null) { previewURLResult = previewURLResult ?? await _getPreviewUrl(file); if (size != null && size < _maxPreviewSizeLimitForCache) { @@ -886,8 +889,9 @@ class VideoPreviewService { "${config.getHttpEndpoint()}/public-collection/files/data/preview", queryParameters: { "fileID": file.uploadedFileID, - "type": - file.fileType == FileType.video ? "vid_preview" : "img_preview", + "type": file.fileType == FileType.video + ? "vid_preview" + : "img_preview", }, options: Options( headers: collectionsService.publicCollectionHeaders( @@ -901,8 +905,9 @@ class VideoPreviewService { "/files/data/preview", queryParameters: { "fileID": file.uploadedFileID, - "type": - file.fileType == FileType.video ? "vid_preview" : "img_preview", + "type": file.fileType == FileType.video + ? "vid_preview" + : "img_preview", }, ); url = (response.data["url"] as String); @@ -1027,12 +1032,14 @@ 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 ??= - await filesDB.getAnyUploadedFile(queueFileId).catchError((e) => null); + queueFile ??= await filesDB + .getAnyUploadedFile(queueFileId) + .catchError((e) => null); if (queueFile == null) { await uploadLocksDB @@ -1128,10 +1135,21 @@ class VideoPreviewService { bool _allowManualStream() { return isVideoStreamingEnabled && - computeController.requestCompute(stream: true, bypassInteractionCheck: true); + computeController.requestCompute( + stream: true, + bypassInteractionCheck: true, + ); } - void queueFiles({Duration duration = const Duration(seconds: 5), bool isManual = false}) { + 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; From 7c22a8bb25cb8498b1bbe7fc078cde79e8851875 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 27 Aug 2025 21:10:18 +0530 Subject: [PATCH 07/10] chore: lint fix --- .../machine_learning/compute_controller.dart | 12 +- .../lib/services/video_preview_service.dart | 107 +++++++++--------- 2 files changed, 59 insertions(+), 60 deletions(-) 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 9536bf9636..32ff15e352 100644 --- a/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart +++ b/mobile/apps/photos/lib/services/machine_learning/compute_controller.dart @@ -70,12 +70,14 @@ class ComputeController { _logger.info('init done '); } - bool requestCompute( - {bool ml = false, - bool stream = false, - bool bypassInteractionCheck = false}) { + bool requestCompute({ + bool ml = false, + bool stream = false, + bool bypassInteractionCheck = false, + }) { _logger.info( - "Requesting compute: ml: $ml, stream: $stream, bypassInteraction: $bypassInteractionCheck"); + "Requesting compute: ml: $ml, stream: $stream, bypassInteraction: $bypassInteractionCheck", + ); if (!_isDeviceHealthy) { _logger.info("Device not healthy, denying request."); return false; diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index f4c229f594..dc46e3db57 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -52,14 +52,14 @@ class VideoPreviewService { bool get _hasQueuedFile => fileQueue.isNotEmpty; VideoPreviewService._privateConstructor() - : serviceLocator = ServiceLocator.instance, - filesDB = FilesDB.instance, - uploadLocksDB = UploadLocksDB.instance, - ffmpegService = IsolatedFfmpegService.instance, - fileMagicService = FileMagicService.instance, - cacheManager = DefaultCacheManager(), - videoCacheManager = VideoCacheManager.instance, - config = Configuration.instance; + : serviceLocator = ServiceLocator.instance, + filesDB = FilesDB.instance, + uploadLocksDB = UploadLocksDB.instance, + ffmpegService = IsolatedFfmpegService.instance, + fileMagicService = FileMagicService.instance, + cacheManager = DefaultCacheManager(), + videoCacheManager = VideoCacheManager.instance, + config = Configuration.instance; VideoPreviewService( this.config, @@ -223,9 +223,8 @@ class VideoPreviewService { // If total is empty then mark all as processed else compute the ratio // of processed files and total remote video files // netProcessedItems = processed / total - final double netProcessedItems = total.isEmpty - ? 1 - : (processed.length / total.length).clamp(0, 1); + final double netProcessedItems = + total.isEmpty ? 1 : (processed.length / total.length).clamp(0, 1); // Store the data and return it final status = netProcessedItems; @@ -331,9 +330,8 @@ class VideoPreviewService { _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.compressing, file: enteFile, - retryCount: forceUpload - ? 0 - : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, + retryCount: + forceUpload ? 0 : _items[enteFile.uploadedFileID!]?.retryCount ?? 0, collectionID: enteFile.collectionID ?? 0, ); _fireVideoPreviewStateChange( @@ -363,11 +361,9 @@ class VideoPreviewService { ? (fileSize * 8) / props!.duration!.inSeconds : null; - final colorTransfer = videoData["color_transfer"] - ?.toString() - .toLowerCase(); - final isHDR = - colorTransfer != null && + final colorTransfer = + videoData["color_transfer"]?.toString().toLowerCase(); + final isHDR = colorTransfer != null && (colorTransfer == "smpte2084" || colorTransfer == "arib-std-b67"); // create temp file & directory for preview generation @@ -439,17 +435,17 @@ class VideoPreviewService { final playlistGenResult = await ffmpegService .runFfmpeg( - // input file path - '-i "${file.path}" ' + - // main params for streaming - command + - // output file path - '$prefix/output.m3u8', - ) + // input file path + '-i "${file.path}" ' + + // main params for streaming + command + + // output file path + '$prefix/output.m3u8', + ) .onError((error, stackTrace) { - _logger.warning("FFmpeg command failed", error, stackTrace); - return {}; - }); + _logger.warning("FFmpeg command failed", error, stackTrace); + return {}; + }); final playlistGenReturnCode = playlistGenResult["returnCode"] as int?; @@ -481,16 +477,16 @@ class VideoPreviewService { // Fetch resolution of generated stream by decrypting a single frame final playlistFrameResult = await ffmpegService .runFfmpeg( - '-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"', - ) + '-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"', + ) .onError((error, stackTrace) { - _logger.warning( - "FFmpeg command failed for frame", - error, - stackTrace, - ); - return {}; - }); + _logger.warning( + "FFmpeg command failed for frame", + error, + stackTrace, + ); + return {}; + }); final playlistFrameReturnCode = playlistFrameResult["returnCode"] as int?; int? width, height; @@ -663,13 +659,16 @@ class VideoPreviewService { try { final encryptionKey = getFileKey(file); final playlistContent = playlist.readAsStringSync(); - final result = await gzipAndEncryptJson({ - "playlist": playlistContent, - 'type': 'hls_video', - 'width': width, - 'height': height, - 'size': objectSize, - }, encryptionKey); + final result = await gzipAndEncryptJson( + { + "playlist": playlistContent, + 'type': 'hls_video', + 'width': width, + 'height': height, + 'size': objectSize, + }, + encryptionKey, + ); final _ = await serviceLocator.enteDio.put( "/files/video-data", data: { @@ -788,7 +787,8 @@ class VideoPreviewService { } final videoFile = (await videoCacheManager.getFileFromCache( _getVideoPreviewKey(objectID), - ))?.file; + )) + ?.file; if (videoFile == null) { previewURLResult = previewURLResult ?? await _getPreviewUrl(file); if (size != null && size < _maxPreviewSizeLimitForCache) { @@ -889,9 +889,8 @@ class VideoPreviewService { "${config.getHttpEndpoint()}/public-collection/files/data/preview", queryParameters: { "fileID": file.uploadedFileID, - "type": file.fileType == FileType.video - ? "vid_preview" - : "img_preview", + "type": + file.fileType == FileType.video ? "vid_preview" : "img_preview", }, options: Options( headers: collectionsService.publicCollectionHeaders( @@ -905,9 +904,8 @@ class VideoPreviewService { "/files/data/preview", queryParameters: { "fileID": file.uploadedFileID, - "type": file.fileType == FileType.video - ? "vid_preview" - : "img_preview", + "type": + file.fileType == FileType.video ? "vid_preview" : "img_preview", }, ); url = (response.data["url"] as String); @@ -1037,9 +1035,8 @@ class VideoPreviewService { ); // If not found in 60-day list, fetch it individually - queueFile ??= await filesDB - .getAnyUploadedFile(queueFileId) - .catchError((e) => null); + queueFile ??= + await filesDB.getAnyUploadedFile(queueFileId).catchError((e) => null); if (queueFile == null) { await uploadLocksDB From 04aaa3a5e4c7281f34f860443e3a74fda519d1a0 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Thu, 28 Aug 2025 10:25:45 +0000 Subject: [PATCH 08/10] fix: bypass size/duration limits for manual video stream requests Allow manual stream requests to bypass the 500MB file size and 60-second duration limits by passing isManual parameter to _checkFileForPreviewCreation. This ensures users can manually process large files even if they exceed the automatic streaming limits. --- .../apps/photos/lib/services/video_preview_service.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index dc46e3db57..929df89ae3 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -299,7 +299,7 @@ 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; @@ -922,8 +922,9 @@ 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); @@ -936,7 +937,7 @@ class VideoPreviewService { } final int size = enteFile.fileSize!; final int duration = enteFile.duration!; - if (size >= 500 * 1024 * 1024 || duration > 60) { + if (!isManual && (size >= 500 * 1024 * 1024 || duration > 60)) { _logger.info("Skip Preview due to size: $size or duration: $duration"); return (null, true, null); } From fa7ccbd180a065a3f541a34f85ea9ec820fd7af1 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Thu, 28 Aug 2025 16:16:15 +0530 Subject: [PATCH 09/10] fix: if fileSize is null for manual way then skip 10MB check --- .../lib/services/video_preview_service.dart | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/mobile/apps/photos/lib/services/video_preview_service.dart b/mobile/apps/photos/lib/services/video_preview_service.dart index 929df89ae3..49f47f4e81 100644 --- a/mobile/apps/photos/lib/services/video_preview_service.dart +++ b/mobile/apps/photos/lib/services/video_preview_service.dart @@ -299,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, isManual); + var (props, result, file) = + await _checkFileForPreviewCreation(enteFile, isManual); if (result) { removeFile = true; return; @@ -929,21 +930,28 @@ class VideoPreviewService { _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 (!isManual && (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) { From 3685cd21543c0b1dcf7e023fb08d3fa28efe8acf Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Thu, 28 Aug 2025 17:34:29 +0530 Subject: [PATCH 10/10] fix: don't show create stream if file size is null --- mobile/apps/photos/lib/ui/viewer/file/file_app_bar.dart | 1 + 1 file changed, 1 insertion(+) 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; }