diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2e3645f..87bb955 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -53,6 +53,7 @@ android { buildTypes { release { + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") } } @@ -76,3 +77,11 @@ kotlin { flutter { source = "../.." } + +// 다운로드 (앱 안 브라우저 · 다운로드 목록): Android 용 yt-dlp (Python 포함) · ffmpeg (영상 · 음성 합치기) · aria2c (토렌트) +// 실행 파일을 앱에 넣고 처음 켤 때 풀어 둔다. 실행은 Dart (YtDlpBackend · Aria2Backend) 가 Windows 와 같은 방식으로 한다. +dependencies { + implementation("io.github.junkfood02.youtubedl-android:library:0.18.1") + implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.18.1") + implementation("io.github.junkfood02.youtubedl-android:aria2c:0.18.1") +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..ae174cf --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +# youtubedl-android (Android 다운로드): 코드 축소(R8)가 Jackson · 압축 풀기 클래스를 지우거나 이름을 바꾸면 +# YoutubeDL 객체를 만들 때 ExceptionInInitializerError 가 난다 (youtubedl-android README 의 권장 규칙) +-keep class com.yausername.** { *; } +-keep class com.fasterxml.jackson.** { *; } +-keep class org.apache.commons.compress.archivers.zip.** { *; } +-dontwarn com.fasterxml.jackson.** +-dontwarn org.apache.commons.** diff --git a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt index 8deaee7..ec5a36d 100644 --- a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt +++ b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt @@ -7,11 +7,15 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings +import com.yausername.aria2c.Aria2c +import com.yausername.ffmpeg.FFmpeg +import com.yausername.youtubedl_android.YoutubeDL import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.io.File -/// 저장소 전체 접근 (동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위해) 확인 · 요청 +/// 저장소 전체 접근 (동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위해) 확인 · 요청, 다운로드 프로그램 준비 class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -23,11 +27,61 @@ class MainActivity : FlutterActivity() { result.success(null) } "storageRoot" -> result.success(Environment.getExternalStorageDirectory().path) + "downloadToolsInit" -> inBackground(result) { downloadToolsInit() } + "downloadToolsUpdate" -> inBackground(result) { + YoutubeDL.updateYoutubeDL(applicationContext)?.name ?: "" + } else -> result.notImplemented() } } } + /// 오래 걸리는 일 (처음 켤 때 실행 파일 풀기 · yt-dlp 업데이트) 은 메인 스레드 밖에서 + private fun inBackground(result: MethodChannel.Result, work: () -> Any?) { + Thread { + try { + val value = work() + runOnUiThread { result.success(value) } + } catch (e: Throwable) { + android.util.Log.e("jj_mkvmaker", "download tools", e) + val cause = generateSequence(e) { it.cause }.last() + runOnUiThread { result.error("TOOLS", "${e.javaClass.simpleName}: ${cause.message ?: cause}", null) } + } + }.start() + } + + /// youtubedl-android 가 앱에 넣은 Python · yt-dlp · ffmpeg · aria2c 를 풀고, 직접 실행할 경로 · 환경 변수를 돌려준다. + /// 경로 규칙은 youtubedl-android 0.18.1 의 YoutubeDL.init / FFmpeg.init / Aria2c.init 과 같다. + private fun downloadToolsInit(): Map { + YoutubeDL.init(applicationContext) + FFmpeg.init(applicationContext) + Aria2c.init(applicationContext) + val bin = applicationInfo.nativeLibraryDir + val base = File(noBackupFilesDir, YoutubeDL.baseName) + val packages = File(base, "packages") + val python = File(packages, "python").absolutePath + val ffmpeg = File(packages, "ffmpeg").absolutePath + val aria2c = File(packages, "aria2c").absolutePath + val cert = "$python/usr/etc/tls/cert.pem" + return mapOf( + "python" to "$bin/libpython.so", + "ytdlp" to File(File(base, YoutubeDL.ytdlpDirName), YoutubeDL.ytdlpBin).absolutePath, + "ffmpeg" to "$bin/libffmpeg.so", + "quickjs" to "$bin/libqjs.so", + "aria2c" to "$bin/libaria2c.so", + "cert" to cert, + "version" to (YoutubeDL.versionName(applicationContext) ?: ""), + "env" to mapOf( + "LD_LIBRARY_PATH" to "$python/usr/lib:$ffmpeg/usr/lib:$aria2c/usr/lib", + "SSL_CERT_FILE" to cert, + "PATH" to (System.getenv("PATH") ?: "") + ":" + bin, + "PYTHONHOME" to "$python/usr", + "HOME" to "$python/usr", + "TMPDIR" to cacheDir.absolutePath, + ), + ) + } + private fun hasAllFilesAccess(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Environment.isExternalStorageManager() diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 946c361..84e7623 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -203,6 +203,8 @@ class AppSettings { /// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더) String resolvedDownloadRoot() { if (downloadRoot != null && downloadRoot!.isNotEmpty) return downloadRoot!; + // Android: 프로그램 폴더가 없으므로 내장 저장소의 Download\JJ_MKVMaker + if (Platform.isAndroid) return '/storage/emulated/0/Download/JJ_MKVMaker'; final appDir = p.dirname(Platform.resolvedExecutable); try { final probe = File(p.join(appDir, '.jj_write_test')); diff --git a/lib/main.dart b/lib/main.dart index c2f1802..b291dc1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'app/download_manager.dart'; import 'app/settings.dart'; import 'core/app_update.dart'; import 'core/playlist.dart'; +import 'platform/android/android_download_tools.dart'; import 'platform/android/android_storage.dart'; import 'platform/windows/app_paths.dart'; import 'platform/windows/cef_runtime.dart'; @@ -329,10 +330,36 @@ Future runAndroid(String dataDir) async { final messengerKey = GlobalKey(); AndroidStorageService.navigatorKey = navigatorKey; + // 다운로드 (앱 안 브라우저의 [다운로드] · 다운로드 목록): 앱에 넣은 yt-dlp · aria2c. 클립보드 감시는 하지 않는다. + AndroidDownloadTools.log = controller.note; + final downloads = DownloadManager( + backends: services.createDownloadBackends?.call(() => controller.settings) ?? const [], + settings: () => controller.settings, + ); + downloads.onAdded.listen((t) => messengerKey.currentState + ?.showSnackBar(SnackBar(content: Text('다운로드 추가: ${t.source}')))); + // 다 받은 동영상 → 편집 목록 (설정에서 끌 수 있음) + downloads.onFinished.listen((t) async { + if (!controller.settings.addFinishedDownloads) return; + final files = DownloadManager.videoFilesOf(t); + if (files.isEmpty) return; + final n = await controller.addDownloaded(files); + downloads.dropFromList(t); + if (n > 0) { + messengerKey.currentState?.showSnackBar(SnackBar(content: Text('동영상 목록에 추가: ${t.title}'))); + } + }); + downloads.setAutoAdd = (on) async { + await controller.updateSettings((x) => x.addFinishedDownloads = on); + downloads.refresh(); + }; + downloads.addToEditList = controller.addDownloaded; + downloads.log = controller.note; + WidgetsBinding.instance.addPostFrameCallback((_) async { if (controller.settings.startScreen == 'browser') { - navigatorKey.currentState - ?.push(MaterialPageRoute(builder: (_) => BrowserPage(c: controller, bookmarks: bookmarks))); + navigatorKey.currentState?.push(MaterialPageRoute( + builder: (_) => BrowserPage(c: controller, downloads: downloads, bookmarks: bookmarks))); } // 동영상을 읽고 옆에 MKV 를 만들려면 저장소 전체 접근이 필요하다 if (!await AndroidAccess.hasAllFiles()) { @@ -346,11 +373,13 @@ Future runAndroid(String dataDir) async { runApp(JjCapCutApp( controller: controller, + downloads: downloads, bookmarks: bookmarks, navigatorKey: navigatorKey, messengerKey: messengerKey, onExit: () async { final ctx = navigatorKey.currentContext; + if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return; if (controller.busy && ctx != null && ctx.mounted) { final ok = await showDialog( context: ctx, @@ -366,6 +395,7 @@ Future runAndroid(String dataDir) async { if (ok != true) return; } controller.cancel(); + await downloads.shutdown(); controller.note('── 정상 종료 ──'); await services.shell.quit(); }, diff --git a/lib/platform/android/android_download_tools.dart b/lib/platform/android/android_download_tools.dart new file mode 100644 index 0000000..62d9843 --- /dev/null +++ b/lib/platform/android/android_download_tools.dart @@ -0,0 +1,139 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import '../../app/settings.dart'; +import '../../core/download_detect.dart'; +import '../../services/downloader.dart'; +import '../windows/aria2_backend.dart'; +import '../windows/ytdlp_backend.dart'; + +/// Android 의 다운로드 프로그램 (youtubedl-android 가 앱에 넣은 Python · yt-dlp · ffmpeg · aria2c). +/// MainActivity.kt 의 "downloadToolsInit" 이 처음 켤 때 풀고, 직접 실행할 경로 · 환경 변수를 알려 준다. +/// 실행은 Windows 와 같은 [YtDlpBackend] · [Aria2Backend] 가 한다. +class AndroidDownloadTools { + static const _ch = MethodChannel('jj_mkvmaker/android'); + + final String python, ytdlp, ffmpeg, quickjs, aria2c, cert, version; + final Map environment; + + AndroidDownloadTools._(Map m) + : python = m['python'] as String, + ytdlp = m['ytdlp'] as String, + ffmpeg = m['ffmpeg'] as String, + quickjs = m['quickjs'] as String, + aria2c = m['aria2c'] as String, + cert = m['cert'] as String, + version = m['version'] as String? ?? '', + environment = (m['env'] as Map).cast(); + + static Future? _ready; + + /// 작업 기록에 남길 곳 (main 에서 정함) + static void Function(String)? log; + + /// 한 번만 준비한다 (처음 켤 때 Python 등을 푸느라 몇 초 걸림). + /// YouTube 가 바뀌면 오래된 yt-dlp 는 받지 못하므로 준비할 때 새 yt-dlp 가 있으면 받는다 (못 받아도 계속). + static Future ready() { + final f = _ready ??= _prepare(); + // 실패하면 (저장 공간 부족 등) 다음에 다시 시도 + f.then( + (_) {}, + onError: (Object _) { + if (identical(_ready, f)) _ready = null; + }, + ); + return f; + } + + static Future _prepare() async { + final m = await _ch.invokeMethod>('downloadToolsInit'); + try { + final r = await _ch.invokeMethod('downloadToolsUpdate').timeout(const Duration(seconds: 30)); + if (r == 'DONE') log?.call('yt-dlp 를 새 버전으로 바꿨습니다'); + } catch (e) { + log?.call('yt-dlp 업데이트 확인 실패 (지금 버전으로 받습니다): $e'); + } + final tools = AndroidDownloadTools._(m!); + log?.call('다운로드 준비됨: yt-dlp ${tools.version}'); + return tools; + } + + /// 다운로드 엔진: yt-dlp (동영상 사이트) · aria2 (토렌트). 실제 엔진은 처음 쓸 때 준비된다. + static List backends(AppSettings Function() s) => [ + _Deferred(DownloadKind.video, () async { + final t = await ready(); + return YtDlpBackend( + ytdlp: t.python, + prefixArgs: [t.ytdlp, '--no-cache-dir'], + environment: t.environment, + // --ffmpeg-location 은 파일 경로도 받는다 (libffmpeg.so = ffmpeg 실행 파일) + ffmpegDir: t.ffmpeg, + jsRuntime: 'quickjs:${t.quickjs}', + formatArgs: () => ytDlpFormatArgs(s().ytContainer, s().ytQuality), + cookieArgs: () => ytDlpCookieArgs( + browser: s().ytCookiesBrowser, + file: s().ytCookiesFile, + internalCookieFile: s().internalCookieFile, + ), + fallbackCookieArgs: () => + ytDlpCookieArgs(browser: internalBrowserCookies, internalCookieFile: s().internalCookieFile), + ); + }), + _Deferred(DownloadKind.torrent, () async { + final t = await ready(); + return Aria2Backend(aria2c: t.aria2c, environment: t.environment, extraArgs: ['--ca-certificate=${t.cert}']); + }), + ]; +} + +/// 처음 쓸 때 엔진을 준비하는 다운로드 엔진 (앱 시작을 늦추지 않도록) +class _Deferred implements DownloadBackend { + @override + final DownloadKind kind; + final Future Function() _create; + Future? _backend; + + _Deferred(this.kind, this._create); + + Future get _b { + final f = _backend ??= _create(); + // 실패하면 다음에 다시 시도 + f.then( + (_) {}, + onError: (Object _) { + if (identical(_backend, f)) _backend = null; + }, + ); + return f; + } + + @override + Future<(String, List)?> expandPlaylist(String url) async => (await _b).expandPlaylist(url); + + @override + Future start(DownloadTask t, void Function() changed) async { + final DownloadBackend b; + try { + b = await _b; + } catch (e) { + t + ..state = DownloadState.failed + ..error = '다운로드 프로그램을 준비할 수 없습니다: $e'; + changed(); + return; + } + return b.start(t, changed); + } + + @override + Future pause(DownloadTask t) async => (await _b).pause(t); + + @override + Future cancel(DownloadTask t) async => (await _b).cancel(t); + + @override + Future shutdown() async { + if (_backend != null) await (await _backend!).shutdown(); + } +} diff --git a/lib/platform/windows/aria2_backend.dart b/lib/platform/windows/aria2_backend.dart index 154cc7a..87e3ae5 100644 --- a/lib/platform/windows/aria2_backend.dart +++ b/lib/platform/windows/aria2_backend.dart @@ -22,7 +22,12 @@ class Aria2Backend implements DownloadBackend { Timer? _poll; final _tasks = {}; - Aria2Backend({String? aria2c}) : aria2c = aria2c ?? locateTool('aria2c'); + /// 추가 환경 변수 · 인수 (Android: 라이브러리 위치 · 인증서 파일) + final Map environment; + final List extraArgs; + + Aria2Backend({String? aria2c, this.environment = const {}, this.extraArgs = const []}) + : aria2c = aria2c ?? locateTool('aria2c'); @override DownloadKind get kind => DownloadKind.torrent; @@ -48,7 +53,8 @@ class Aria2Backend implements DownloadBackend { '--bt-save-metadata=false', '--follow-torrent=mem', '--console-log-level=warn', '--summary-interval=0', '--file-allocation=none', '--auto-file-renaming=false', - ]); + ...extraArgs, + ], environment: environment.isEmpty ? null : environment); _daemon!.stdout.drain(); _daemon!.stderr.drain(); unawaited(_daemon!.exitCode.then((_) => _daemon = null)); diff --git a/lib/platform/windows/ytdlp_backend.dart b/lib/platform/windows/ytdlp_backend.dart index 2264d4e..5f28b1d 100644 --- a/lib/platform/windows/ytdlp_backend.dart +++ b/lib/platform/windows/ytdlp_backend.dart @@ -27,8 +27,14 @@ class YtDlpBackend implements DownloadBackend { /// yt-dlp 가 영상·음성을 합칠 때 쓰는 ffmpeg 폴더 final String? ffmpegDir; - /// YouTube 추출용 JavaScript 실행기 (없으면 일부 화질이 빠질 수 있음) - final String? deno; + /// YouTube 추출용 JavaScript 실행기 "deno:경로" · "quickjs:경로" (없으면 일부 화질이 빠질 수 있음) + final String? jsRuntime; + + /// yt-dlp 앞에 붙일 인수 (Android: python 으로 yt-dlp 스크립트를 실행 → [ytdlp] 는 python, 여기에 스크립트 경로) + final List prefixArgs; + + /// 추가 환경 변수 (Android: Python · ffmpeg 라이브러리 위치 등) + final Map environment; final _procs = {}; final _stopping = {}; @@ -53,16 +59,22 @@ class YtDlpBackend implements DownloadBackend { String? ytdlp, this.ffmpegDir, String? deno, + String? jsRuntime, + this.prefixArgs = const [], + this.environment = const {}, List Function()? formatArgs, List Function()? cookieArgs, List Function()? fallbackCookieArgs, }) : ytdlp = ytdlp ?? locateTool('yt-dlp'), - deno = deno ?? _existing(locateTool('deno')), + jsRuntime = jsRuntime ?? _denoRuntime(deno ?? _existing(locateTool('deno'))), formatArgs = formatArgs ?? (() => ytDlpFormatArgs(YtContainer.mp4, YtQuality.best)), cookieArgs = cookieArgs ?? (() => const []), fallbackCookieArgs = fallbackCookieArgs ?? (() => const []); - static const _env = {'PYTHONIOENCODING': 'utf-8', 'PYTHONUTF8': '1'}; + static const _pyEnv = {'PYTHONIOENCODING': 'utf-8', 'PYTHONUTF8': '1'}; + Map get _env => {..._pyEnv, ...environment}; + + static String? _denoRuntime(String? deno) => deno == null ? null : 'deno:$deno'; @override Future<(String, List)?> expandPlaylist(String url) async { @@ -72,8 +84,9 @@ class YtDlpBackend implements DownloadBackend { Future read(List cookies) => Process.run( ytdlp, [ + ...prefixArgs, '--flat-playlist', '-J', '--no-warnings', '--encoding', 'utf-8', - if (deno != null) ...['--js-runtimes', 'deno:$deno'], + if (jsRuntime != null) ...['--js-runtimes', jsRuntime!], ...cookies, 'https://www.youtube.com/playlist?list=$listId', ], @@ -130,7 +143,7 @@ class YtDlpBackend implements DownloadBackend { ...formatArgs(), ..._cookiesFor(t), if (ffmpegDir != null) ...['--ffmpeg-location', ffmpegDir!], - if (deno != null) ...['--js-runtimes', 'deno:$deno'], + if (jsRuntime != null) ...['--js-runtimes', jsRuntime!], t.source, ]; @@ -145,7 +158,7 @@ class YtDlpBackend implements DownloadBackend { final Process proc; try { - proc = await Process.start(ytdlp, argsFor(t), environment: _env); + proc = await Process.start(ytdlp, [...prefixArgs, ...argsFor(t)], environment: _env); } on ProcessException catch (e) { t ..state = DownloadState.failed diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index c8cbe0e..1992a5c 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -9,6 +9,7 @@ import '../core/download_detect.dart'; import '../platform/common/nllb_translator.dart'; import '../platform/common/opensubtitles_provider.dart'; import '../platform/common/whisper_recognizer.dart'; +import '../platform/android/android_download_tools.dart'; import '../platform/android/android_shell.dart'; import '../platform/android/android_storage.dart'; import '../platform/android/ffmpeg_kit_media_tool.dart'; @@ -119,7 +120,7 @@ class PlatformServices { ); } if (Platform.isAndroid) { - // 다운로드 (yt-dlp · aria2) · 업데이트 · 탐색기 연결은 데스크톱 전용 + // 업데이트 · 탐색기 연결은 데스크톱 전용. 다운로드는 앱에 넣은 yt-dlp · aria2c (youtubedl-android) MediaKitPreviewPlayer.ensureInitialized(); return PlatformServices( mediaTool: FfmpegKitMediaTool(), @@ -128,6 +129,7 @@ class PlatformServices { createMediaPlayer: MediaKitFullPlayer.new, createRecognizer: WhisperRecognizer.new, createTranslator: NllbTranslator.new, + createDownloadBackends: AndroidDownloadTools.backends, shell: AndroidShell(), createSubtitleProviders: (s) => [ OpenSubtitlesProvider( diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index fb02adf..2723320 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -371,25 +371,24 @@ class _SettingsPageState extends State { }, onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null), ), - if (desk) ...[ _folderTile( title: '다운로드 (jj_yt-dlp · jj_aria2)', value: s.downloadRoot, - defaultText: '프로그램 폴더 (기본): ${s.resolvedDownloadRoot()}', + defaultText: '${desk ? '프로그램 폴더' : '내장 저장소'} (기본): ${s.resolvedDownloadRoot()}', onPick: () async { final d = await _pickDir('다운로드 위치'); if (d != null) await c.updateSettings((x) => x.downloadRoot = d); }, onReset: () => c.updateSettings((x) => x.downloadRoot = null), ), - _section('다운로드'), + if (desk) _section('다운로드'), + if (desk) SwitchListTile( value: s.clipboardWatch, onChanged: (v) => c.updateSettings((x) => x.clipboardWatch = v), title: const Text('복사(Ctrl+C)한 주소 자동 다운로드'), subtitle: const Text('YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)'), ), - ], _section('재생'), ListTile( title: const Text('동영상 하나를 재생할 때'), @@ -438,7 +437,6 @@ class _SettingsPageState extends State { (v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())), _textTile('비밀번호 (선택)', s.openSubtitlesPassword, '이 PC 의 설정 파일에 저장됩니다', (v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true), - if (desk) ...[ ListTile( title: const Text('YouTube 받을 형식'), subtitle: const Text('기본: MP4. 음성만 받으면 MP3 · M4A 로 저장'), @@ -479,8 +477,10 @@ class _SettingsPageState extends State { items: [ const DropdownMenuItem(value: '', child: Text('사용 안 함')), const DropdownMenuItem(value: internalBrowserCookies, child: Text('앱 안 브라우저 (권장)')), - for (final b in const ['firefox', 'chrome', 'edge', 'brave', 'whale', 'opera']) - DropdownMenuItem(value: b, child: Text(b[0].toUpperCase() + b.substring(1))), + // PC 브라우저의 쿠키는 Windows 에서만 + if (desk) + for (final b in const ['firefox', 'chrome', 'edge', 'brave', 'whale', 'opera']) + DropdownMenuItem(value: b, child: Text(b[0].toUpperCase() + b.substring(1))), const DropdownMenuItem(value: '*file', child: Text('cookies.txt 파일…')), ], onChanged: (v) async { @@ -511,7 +511,6 @@ class _SettingsPageState extends State { onChanged: (n) => c.updateSettings((x) => x.maxParallelDownloads = n), ), ), - ], _section('변환'), ListTile( title: const Text('동시 MKV 변환 수'), diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock new file mode 100644 index 0000000..34af3b6 Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin new file mode 100644 index 0000000..a81f0e4 Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/sha1-checksums.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/sha1-checksums.bin new file mode 100644 index 0000000..7c5e15b Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/sha1-checksums.bin differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin new file mode 100644 index 0000000..f6c721c Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock new file mode 100644 index 0000000..a423b02 Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/gc.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..7110e2a Binary files /dev/null and b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..e85cbdf --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Fri Oct 02 20:36:01 KST 2026 +gradle.version=8.14.3 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/vcs-1/gc.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29