Skip to content

Commit 966ac24

Browse files
authored
[mob][photos] video editor export hanging issue (#7241)
## Description - [x] Fix Video editor regression ## Tests - [x] Test with video trim to 5 seconds and no-trim but other transformations - [x] Test big videos
2 parents 96a9c10 + 5178138 commit 966ac24

File tree

2 files changed

+169
-43
lines changed

2 files changed

+169
-43
lines changed
Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,160 @@
1+
import 'dart:async';
12
import 'dart:developer';
23
import 'dart:io';
34

45
import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
5-
import 'package:ffmpeg_kit_flutter/ffmpeg_kit_config.dart';
66
import 'package:ffmpeg_kit_flutter/ffmpeg_session.dart';
77
import 'package:ffmpeg_kit_flutter/return_code.dart';
8+
import 'package:ffmpeg_kit_flutter/session_state.dart';
89
import 'package:ffmpeg_kit_flutter/statistics.dart';
10+
import 'package:logging/logging.dart';
911
import 'package:video_editor/video_editor.dart';
1012

1113
class ExportService {
14+
static final _logger = Logger('ExportService');
15+
1216
static Future<void> dispose() async {
17+
_logger.info('[FFmpeg] Disposing export service');
1318
final executions = await FFmpegKit.listSessions();
14-
if (executions.isNotEmpty) await FFmpegKit.cancel();
19+
_logger.info('[FFmpeg] Found ${executions.length} active sessions');
20+
if (executions.isNotEmpty) {
21+
_logger.info('[FFmpeg] Cancelling all sessions');
22+
await FFmpegKit.cancel();
23+
}
1524
}
1625

17-
static Future<FFmpegSession> runFFmpegCommand(
26+
static Future<void> runFFmpegCommand(
1827
FFmpegVideoEditorExecute execute, {
1928
required void Function(File file) onCompleted,
2029
void Function(Object, StackTrace)? onError,
2130
void Function(Statistics)? onProgress,
22-
}) {
31+
}) async {
2332
log('FFmpeg start process with command = ${execute.command}');
24-
return FFmpegKit.executeAsync(
25-
execute.command,
26-
(session) async {
27-
final state =
28-
FFmpegKitConfig.sessionStateToString(await session.getState());
29-
final code = await session.getReturnCode();
30-
31-
if (ReturnCode.isSuccess(code)) {
32-
onCompleted(File(execute.outputPath));
33-
} else {
34-
if (onError != null) {
35-
onError(
36-
Exception(
37-
'FFmpeg process exited with state $state and return code $code.\n${await session.getOutput()}',
38-
),
39-
StackTrace.current,
33+
34+
final completer = Completer<void>();
35+
FFmpegSession? activeSession;
36+
37+
try {
38+
// Run FFmpeg with async callbacks
39+
activeSession = await FFmpegKit.executeAsync(
40+
execute.command,
41+
(session) async {
42+
// Session complete callback
43+
final returnCode = await session.getReturnCode();
44+
final output = await session.getOutput();
45+
46+
if (returnCode != null && ReturnCode.isSuccess(returnCode)) {
47+
final outputFile = File(execute.outputPath);
48+
49+
if (!outputFile.existsSync()) {
50+
_logger.warning(
51+
'Output file does not exist at ${execute.outputPath}',
52+
);
53+
}
54+
55+
onCompleted(outputFile);
56+
} else {
57+
final errorCode = returnCode?.getValue() ?? -1;
58+
_logger.severe('FFmpeg process failed with return code $errorCode');
59+
60+
final error = Exception(
61+
'FFmpeg process exited with return code $errorCode.\n$output',
4062
);
63+
64+
if (onError != null) {
65+
onError(error, StackTrace.current);
66+
}
67+
}
68+
69+
if (!completer.isCompleted) {
70+
completer.complete();
71+
}
72+
},
73+
null, // No log callback
74+
(statistics) {
75+
// Statistics callback for progress
76+
if (onProgress != null) {
77+
onProgress(statistics);
78+
}
79+
},
80+
);
81+
82+
// Poll session state to ensure we wait for completion
83+
bool callbackTriggered = false;
84+
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
85+
if (activeSession != null) {
86+
final state = await activeSession.getState();
87+
final statisticsList = await activeSession.getStatistics();
88+
89+
if (statisticsList != null &&
90+
statisticsList.isNotEmpty &&
91+
onProgress != null) {
92+
final statistics = statisticsList.last;
93+
onProgress(statistics);
94+
}
95+
96+
if (state == SessionState.completed) {
97+
timer.cancel();
98+
99+
// If callback hasn't been triggered yet, do it manually
100+
if (!callbackTriggered && !completer.isCompleted) {
101+
callbackTriggered = true;
102+
103+
// Get the return code and handle completion
104+
final returnCode = await activeSession.getReturnCode();
105+
106+
if (returnCode != null && ReturnCode.isSuccess(returnCode)) {
107+
final outputFile = File(execute.outputPath);
108+
109+
if (!outputFile.existsSync()) {
110+
_logger.warning(
111+
'Output file does not exist at ${execute.outputPath}',
112+
);
113+
}
114+
115+
onCompleted(outputFile);
116+
} else {
117+
final errorCode = returnCode?.getValue() ?? -1;
118+
_logger.severe(
119+
'FFmpeg process failed with return code $errorCode',
120+
);
121+
122+
final error = Exception(
123+
'FFmpeg process exited with return code $errorCode',
124+
);
125+
126+
if (onError != null) {
127+
onError(error, StackTrace.current);
128+
}
129+
}
130+
131+
completer.complete();
132+
}
133+
} else if (state == SessionState.failed) {
134+
timer.cancel();
135+
if (!completer.isCompleted) {
136+
final error = Exception('FFmpeg process failed');
137+
if (onError != null) {
138+
onError(error, StackTrace.current);
139+
}
140+
completer.complete();
141+
}
41142
}
42-
return;
43143
}
44-
},
45-
null,
46-
onProgress,
47-
);
144+
});
145+
146+
// Wait for the session to complete
147+
await completer.future;
148+
} catch (e, stackTrace) {
149+
_logger.severe('FFmpeg execution error: $e');
150+
if (activeSession != null) {
151+
await FFmpegKit.cancel(activeSession.getSessionId());
152+
}
153+
if (onError != null) {
154+
onError(e, stackTrace);
155+
} else {
156+
rethrow;
157+
}
158+
}
48159
}
49160
}

mobile/apps/photos/lib/ui/tools/editor/video_editor_page.dart

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
5959

6060
@override
6161
void initState() {
62-
_logger.info("Initializing video editor page");
6362
super.initState();
6463

6564
Future.microtask(() {
@@ -106,7 +105,6 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
106105

107106
@override
108107
Widget build(BuildContext context) {
109-
_logger.info("Building video editor page");
110108
return PopScope(
111109
canPop: false,
112110
onPopInvokedWithResult: (didPop, _) {
@@ -251,36 +249,47 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
251249

252250
final String startTrimCmd = "-ss ${_controller!.startTrim}";
253251
final String toTrimCmd = "-t ${_controller!.trimmedDuration}";
254-
return '$startTrimCmd -i $videoPath $toTrimCmd ${config.filtersCmd(filters)} -c:v libx264 -c:a aac $outputPath';
252+
final command =
253+
'$startTrimCmd -i $videoPath $toTrimCmd ${config.filtersCmd(filters)} -c:v libx264 -c:a aac $outputPath';
254+
return command;
255255
},
256256
);
257257

258258
try {
259+
final executeConfig = await config.getExecuteConfig();
260+
259261
await ExportService.runFFmpegCommand(
260-
await config.getExecuteConfig(),
262+
executeConfig,
261263
onProgress: (stats) {
264+
final progress = config.getFFmpegProgress(stats.getTime().toInt());
262265
if (dialogKey.currentState != null) {
263-
dialogKey.currentState!
264-
.setProgress(config.getFFmpegProgress(stats.getTime().toInt()));
266+
dialogKey.currentState!.setProgress(progress);
265267
}
266268
},
267-
onError: (e, s) => _logger.severe("Error exporting video", e, s),
269+
onError: (e, s) {
270+
_logger.severe("Error exporting video", e, s);
271+
},
268272
onCompleted: (result) async {
269273
_isExporting.value = false;
270-
if (!mounted) return;
274+
if (!mounted) {
275+
return;
276+
}
271277

272278
final fileName = path.basenameWithoutExtension(widget.file.title!) +
273279
"_edited_" +
274280
DateTime.now().microsecondsSinceEpoch.toString() +
275281
".mp4";
282+
276283
//Disabling notifications for assets changing to insert the file into
277284
//files db before triggering a sync.
278285
await PhotoManager.stopChangeNotify();
279286

280287
try {
281288
final AssetEntity newAsset =
282289
await (PhotoManager.editor.saveVideo(result, title: fileName));
290+
283291
result.deleteSync();
292+
284293
final newFile = await EnteFile.fromAsset(
285294
widget.file.deviceFolder ?? '',
286295
newAsset,
@@ -300,20 +309,24 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
300309
}
301310
}
302311

303-
newFile.generatedID =
304-
await FilesDB.instance.insertAndGetId(newFile);
305-
Bus.instance
306-
.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
312+
newFile.generatedID = await FilesDB.instance.insertAndGetId(
313+
newFile,
314+
);
315+
316+
Bus.instance.fire(
317+
LocalPhotosUpdatedEvent([newFile], source: "editSave"),
318+
);
319+
307320
SyncService.instance.sync().ignore();
321+
308322
showShortToast(context, AppLocalizations.of(context).editsSaved);
309-
_logger.info("Original file " + widget.file.toString());
310-
_logger.info("Saved edits to file " + newFile.toString());
311323
final files = widget.detailPageConfig.files;
312324

313325
// the index could be -1 if the files fetched doesn't contain the newly
314326
// edited files
315-
int selectionIndex = files
316-
.indexWhere((file) => file.generatedID == newFile.generatedID);
327+
int selectionIndex = files.indexWhere(
328+
(file) => file.generatedID == newFile.generatedID,
329+
);
317330
if (selectionIndex == -1) {
318331
files.add(newFile);
319332
selectionIndex = files.length - 1;
@@ -329,12 +342,14 @@ class _VideoEditorPageState extends State<VideoEditorPage> {
329342
),
330343
),
331344
);
332-
} catch (_) {
345+
} catch (e, s) {
346+
_logger.severe("Error in post-processing", e, s);
333347
Navigator.of(dialogKey.currentContext!).pop('dialog');
334348
}
335349
},
336350
);
337-
} catch (_) {
351+
} catch (e, s) {
352+
_logger.severe("Unexpected error in export process", e, s);
338353
Navigator.of(dialogKey.currentContext!).pop('dialog');
339354
} finally {
340355
await PhotoManager.startChangeNotify();

0 commit comments

Comments
 (0)