120 lines
4.1 KiB
Dart
120 lines
4.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart';
|
|
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit_config.dart';
|
|
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffprobe_kit.dart';
|
|
import 'package:ffmpeg_kit_flutter_new_min_gpl/return_code.dart';
|
|
|
|
import '../../core/models.dart';
|
|
import '../../services/media_tool.dart';
|
|
import '../../l10n/tr.dart';
|
|
import '../windows/process_media_tool.dart' show parseEncoderList, parseFfmpegInfo, parseProbeJson;
|
|
|
|
/// Android: 앱에 넣은 FFmpeg 라이브러리 (ffmpeg-kit) 로 실행하는 구현.
|
|
/// 명령 인수는 데스크톱 (ffmpeg.exe) 과 같다. 진행률만 "-progress pipe:1" 대신 통계 콜백으로 받는다.
|
|
class FfmpegKitMediaTool implements MediaTool {
|
|
/// 실행 중인 작업 (취소용)
|
|
final Set<int> _running = {};
|
|
final Set<int> _cancelled = {};
|
|
|
|
@override
|
|
Future<String?> version() async {
|
|
try {
|
|
final v = await FFmpegKitConfig.getFFmpegVersion();
|
|
return v == null ? null : 'ffmpeg version $v (Android)';
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Set<String>> encoders() async {
|
|
try {
|
|
final s = await FFmpegKit.executeWithArguments(['-hide_banner', '-encoders']);
|
|
return parseEncoderList(await s.getOutput() ?? '');
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<MediaInfo> probe(String path) async {
|
|
try {
|
|
final s = await FFprobeKit.executeWithArguments(
|
|
['-v', 'error', '-print_format', 'json', '-show_format', '-show_streams', path]);
|
|
if (ReturnCode.isSuccess(await s.getReturnCode())) {
|
|
final out = await s.getOutput() ?? '';
|
|
final start = out.indexOf('{');
|
|
if (start >= 0) return parseProbeJson(out.substring(start));
|
|
}
|
|
} catch (_) {}
|
|
// ffprobe 로 안 되면 ffmpeg -i 의 출력으로
|
|
final s = await FFmpegKit.executeWithArguments(['-hide_banner', '-nostdin', '-i', path]);
|
|
final text = await s.getOutput() ?? '';
|
|
final info = parseFfmpegInfo(text);
|
|
if (info.streams.isEmpty) {
|
|
throw MediaToolException(trf('파일을 분석할 수 없습니다: {0}', [text.trim().split('\n').last]));
|
|
}
|
|
return info;
|
|
}
|
|
|
|
@override
|
|
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress}) async {
|
|
// 표준 출력으로 진행률을 쓰는 인수는 빼고 통계 콜백을 쓴다
|
|
final a = <String>[];
|
|
for (var i = 0; i < args.length; i++) {
|
|
if (args[i] == '-progress' && i + 1 < args.length) {
|
|
i++;
|
|
continue;
|
|
}
|
|
a.add(args[i]);
|
|
}
|
|
final totalMs = duration?.inMilliseconds ?? 0;
|
|
final done = Completer<void>();
|
|
final errTail = <String>[];
|
|
var lastProgress = 0.0;
|
|
final session = await FFmpegKit.executeWithArgumentsAsync(
|
|
a,
|
|
(s) async {
|
|
final id = s.getSessionId();
|
|
_running.remove(id);
|
|
final rc = await s.getReturnCode();
|
|
if (_cancelled.remove(id) || ReturnCode.isCancel(rc)) {
|
|
done.completeError(MediaToolException(tr('사용자가 취소했습니다.')));
|
|
} else if (!ReturnCode.isSuccess(rc)) {
|
|
done.completeError(MediaToolException(trf('ffmpeg 오류 (코드 {0})\n{1}', [rc?.getValue(), errTail.join('\n')])));
|
|
} else {
|
|
done.complete();
|
|
}
|
|
},
|
|
(log) {
|
|
for (final line in log.getMessage().split('\n')) {
|
|
if (line.trim().isEmpty) continue;
|
|
errTail.add(line);
|
|
if (errTail.length > 15) errTail.removeAt(0);
|
|
}
|
|
},
|
|
(st) {
|
|
if (onProgress == null || totalMs <= 0 || st.getTime() <= 0) return;
|
|
// 화면을 다시 그리는 일이 많지 않도록 0.5% 이상 바뀔 때만 알린다
|
|
final x = (st.getTime() / totalMs).clamp(0.0, 1.0);
|
|
if (x - lastProgress < 0.005) return;
|
|
lastProgress = x;
|
|
onProgress(x);
|
|
},
|
|
);
|
|
final id = session.getSessionId();
|
|
if (id != null && !done.isCompleted) _running.add(id);
|
|
await done.future;
|
|
onProgress?.call(1.0);
|
|
}
|
|
|
|
@override
|
|
void cancel() {
|
|
for (final id in _running.toList()) {
|
|
_cancelled.add(id);
|
|
unawaited(FFmpegKit.cancel(id));
|
|
}
|
|
}
|
|
}
|