Android 판 (APK): FFmpeg (ffmpeg-kit) 로 MKV 만들기, 앱 안 파일 고르기, 저장소 접근 권한, Windows 전용 설정 숨김

- 플러그인 사본: flutter_inappwebview_android (AGP 9 빌드 오류), ffmpeg_kit_flutter_new_min_gpl (Android 만), whisper_ggml (같은 FFmpeg 사용)
- 64비트 ARM 만, 배포 키 서명 (android/key.properties, 저장소에 올리지 않음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-02 12:46:08 +09:00
parent 2713517456
commit bf6429dc76
282 changed files with 37934 additions and 36 deletions

5
.gitignore vendored
View File

@ -59,3 +59,8 @@ app.*.map.json
# 내장 Chrome: 빌드할 때 내려받는 CEF 배포본 (약 330MB)
/packages/webview_cef/third/cef/
# Android 빌드 중간 파일
/android/build/
/packages/*/android/.cxx/
/packages/*/android/build/

View File

@ -14,7 +14,9 @@ analyzer:
- build/**
- android/**
- windows/**
- packages/webview_cef/** # 고친 외부 부품 (원본 코드 그대로)
- packages/webview_cef/**
- packages/flutter_inappwebview_android/**
- packages/ffmpeg_kit_flutter_new_min_gpl/** # 고친 외부 부품 (원본 코드 그대로)
linter:
# The lint rules applied to this project can be customized in the

View File

@ -1,13 +1,22 @@
import java.util.Properties
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// 배포 서명 키 (android/key.properties, 저장소에 올리지 않음). 없으면 디버그 키로 서명.
val keyProps = Properties().apply {
val f = rootProject.file("key.properties")
if (f.exists()) f.inputStream().use { load(it) }
}
android {
namespace = "com.jj.jj_mkvmaker"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
// whisper_ggml 이 NDK 29 를 요구 (더 낮은 버전용 코드와 호환)
ndkVersion = "29.0.13113456"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@ -15,25 +24,41 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.jj.jj_mkvmaker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
// ffmpeg-kit 은 Android 7.0 (API 24) 이상
minSdk = maxOf(24, flutter.minSdkVersion)
targetSdk = flutter.targetSdkVersion
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
// You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true`
// flag during build.
versionCode = flutter.versionCode
versionName = flutter.versionName
// 요즘 휴대폰 · 태블릿 (64비트 ARM) 만. 플러그인이 다른 CPU 용 라이브러리를 넣어도 앱 본체가 없으면 실행되지 않으므로 뺀다
ndk {
abiFilters += listOf("arm64-v8a")
}
}
signingConfigs {
if (keyProps.getProperty("storeFile") != null) {
create("release") {
storeFile = file(keyProps.getProperty("storeFile"))
storePassword = keyProps.getProperty("storePassword")
keyAlias = keyProps.getProperty("keyAlias")
keyPassword = keyProps.getProperty("keyPassword")
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
}
}
// ffmpeg · whisper 등 네이티브 라이브러리를 압축하지 않고 그대로 (설치 후 바로 로드)
packaging {
jniLibs {
useLegacyPackaging = true
// 64비트 ARM 만 (위 abiFilters 와 같은 뜻, 플러그인 라이브러리까지 확실히 빼기)
excludes += setOf("lib/x86_64/**", "lib/x86/**", "lib/armeabi-v7a/**")
}
}
}

View File

@ -1,8 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- 동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위한 저장소 접근 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:label="jj_mkvmaker"
android:label="JJ_MKVMaker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -1,5 +1,51 @@
package com.jj.jj_mkvmaker
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity()
/// 저장소 전체 접근 (동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위해) 확인 · 요청
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "jj_mkvmaker/android").setMethodCallHandler { call, result ->
when (call.method) {
"hasAllFilesAccess" -> result.success(hasAllFilesAccess())
"requestAllFilesAccess" -> {
requestAllFilesAccess()
result.success(null)
}
"storageRoot" -> result.success(Environment.getExternalStorageDirectory().path)
else -> result.notImplemented()
}
}
}
private fun hasAllFilesAccess(): Boolean =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
private fun requestAllFilesAccess() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
startActivity(Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:$packageName")))
} catch (e: Exception) {
startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
}
} else {
requestPermissions(
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1
)
}
}
}

View File

@ -15,6 +15,12 @@ subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
// 오래된 플러그인 (onnxruntime 등) 은 낮은 compileSdk 로 되어 있어 새 라이브러리와 맞지 않는다 → 모두 36 으로
subprojects {
afterEvaluate {
(extensions.findByName("android") as? com.android.build.gradle.BaseExtension)?.compileSdkVersion(36)
}
}
subprojects {
project.evaluationDependsOn(":app")
}

View File

@ -1,4 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
# 이 PC 는 메모리가 빠듯해서 작게 (원래 -Xmx8G)
org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=256m
org.gradle.workers.max=2
kotlin.daemon.jvmargs=-Xmx1G
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@ -13,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_storage.dart';
import 'platform/windows/app_paths.dart';
import 'platform/windows/cef_runtime.dart';
import 'platform/windows/com_guard.dart';
@ -49,6 +51,7 @@ Future<String> _prepare() async {
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final dataDir = await _prepare();
if (Platform.isAndroid) return runAndroid(dataDir);
// 이미 실행 중이면 인수(탐색기에서 고른 파일)를 넘기고 끝낸다
final request = LaunchRequest.parse(args);
@ -310,6 +313,65 @@ String lastWorkText(AppController c) {
return c.logs.last.replaceFirst(RegExp(r'^\[[\d:]+\]\s*'), '').split('\n').first;
}
/// Android: 창 하나. 트레이 · 단축키 · 탐색기 연결 · 다운로드 (yt-dlp · aria2) · 업데이트는 없다.
/// 데스크톱 화면을 그대로 쓰므로 가로 화면으로 띄운다.
Future<void> runAndroid(String dataDir) async {
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
final services = PlatformServices.create();
final logs = Directory(p.join(dataDir, 'Logs'))..createSync(recursive: true);
final controller = AppController(services, settingsStore: SettingsStore())..logFile = p.join(logs.path, 'app.log');
controller.note('── 시작 $appTitle (Android) ──');
await controller.init();
final bookmarks = BookmarksController();
await bookmarks.load();
final navigatorKey = GlobalKey<NavigatorState>();
final messengerKey = GlobalKey<ScaffoldMessengerState>();
AndroidStorageService.navigatorKey = navigatorKey;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (controller.settings.startScreen == 'browser') {
navigatorKey.currentState
?.push(MaterialPageRoute<void>(builder: (_) => BrowserPage(c: controller, bookmarks: bookmarks)));
}
// 동영상을 읽고 옆에 MKV 를 만들려면 저장소 전체 접근이 필요하다
if (!await AndroidAccess.hasAllFiles()) {
messengerKey.currentState?.showSnackBar(SnackBar(
duration: const Duration(seconds: 20),
content: const Text('동영상을 고르고 MKV 를 만들려면 "모든 파일에 대한 접근" 권한이 필요합니다.'),
action: SnackBarAction(label: '허용', onPressed: AndroidAccess.request),
));
}
});
runApp(JjCapCutApp(
controller: controller,
bookmarks: bookmarks,
navigatorKey: navigatorKey,
messengerKey: messengerKey,
onExit: () async {
final ctx = navigatorKey.currentContext;
if (controller.busy && ctx != null && ctx.mounted) {
final ok = await showDialog<bool>(
context: ctx,
builder: (c) => AlertDialog(
title: const Text('종료'),
content: Text('진행 중인 작업이 있습니다: ${controller.currentJob ?? ''}\n중지하고 끝낼까요?'),
actions: [
TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('취소')),
FilledButton(onPressed: () => Navigator.pop(c, true), child: const Text('종료')),
],
),
);
if (ok != true) return;
}
controller.cancel();
controller.note('── 정상 종료 ──');
await services.shell.quit();
},
));
}
/// 탐색기에서 연 동영상의 새 창: 바로 재생하고, ← 로 나오면 같은 동영상 목록의 MKV 화면.
/// 트레이 · 단축키 · 다운로드 · 브라우저는 원래 창이 맡는다. 닫으면 이 창만 끝난다.
/// 동영상 목록은 원래 창과 같은 파일을 보므로 어느 창에서 넣거나 지워도 서로 맞춰진다.

View File

@ -0,0 +1,20 @@
import 'package:flutter/services.dart';
import '../../services/app_shell.dart';
/// Android: 창 · 트레이 · 단축키 · 탐색기 연결은 없음. 종료와 전체 화면만.
class AndroidShell extends NoopShell {
bool _full = false;
@override
Future<void> quit() => SystemNavigator.pop();
@override
Future<void> setFullScreen(bool on) async {
_full = on;
await SystemChrome.setEnabledSystemUIMode(on ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge);
}
@override
Future<bool> isFullScreen() async => _full;
}

View File

@ -0,0 +1,67 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../core/subtitle_detector.dart';
import '../../ui/android_file_browser.dart';
import '../windows/desktop_storage_service.dart';
/// Android 저장소 전체 접근 (MainActivity.kt 의 "jj_mkvmaker/android")
class AndroidAccess {
static const _ch = MethodChannel('jj_mkvmaker/android');
static Future<bool> hasAllFiles() async {
try {
return await _ch.invokeMethod<bool>('hasAllFilesAccess') ?? false;
} catch (_) {
return false;
}
}
/// 설정 화면 (모든 파일 접근 허용) 을 연다
static Future<void> request() async {
try {
await _ch.invokeMethod<void>('requestAllFilesAccess');
} catch (_) {}
}
/// 내장 저장소 맨 위 (예: /storage/emulated/0)
static Future<String> storageRoot() async {
try {
return await _ch.invokeMethod<String>('storageRoot') ?? '/storage/emulated/0';
} catch (_) {
return '/storage/emulated/0';
}
}
}
/// Android: 실제 파일 경로가 필요하다 (MKV 를 동영상 옆 jj_mkv 폴더에 만든다).
/// 시스템 파일 선택기는 사본을 앱 캐시에 만들어 큰 동영상에 맞지 않으므로 앱 안 파일 고르기 화면을 쓴다.
class AndroidStorageService extends DesktopStorageService {
/// 파일 고르기 화면을 띄울 곳 (main 에서 정함)
static GlobalKey<NavigatorState>? navigatorKey;
Future<List<String>> _pick(String title, List<String> extensions, {String? initialDirectory}) async {
final ctx = navigatorKey?.currentContext;
if (ctx == null) return [];
return await showAndroidFileBrowser(ctx, title: title, extensions: extensions, initialDirectory: initialDirectory) ??
const [];
}
@override
Future<List<String>> pickVideos() => _pick('동영상 선택', videoExtensions);
@override
Future<List<String>> pickSubtitles({String? initialDirectory}) =>
_pick('자막 파일 선택', subtitleExtensions, initialDirectory: initialDirectory);
@override
Future<String> tempDirectory() async {
final dir = Directory(p.join((await getTemporaryDirectory()).path, 'jj_mkvmaker'));
await dir.create(recursive: true);
return dir.path;
}
}

View File

@ -0,0 +1,114 @@
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 '../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('파일을 분석할 수 없습니다: ${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>[];
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(const MediaToolException('사용자가 취소했습니다.'));
} else if (!ReturnCode.isSuccess(rc)) {
done.completeError(MediaToolException('ffmpeg 오류 (코드 ${rc?.getValue()})\n${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) {
onProgress((st.getTime() / totalMs).clamp(0.0, 1.0));
}
},
);
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));
}
}
}

View File

@ -9,6 +9,9 @@ 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_shell.dart';
import '../platform/android/android_storage.dart';
import '../platform/android/ffmpeg_kit_media_tool.dart';
import '../platform/windows/aria2_backend.dart';
import '../platform/windows/desktop_shell.dart';
import '../platform/windows/desktop_storage_service.dart';
@ -115,6 +118,26 @@ class PlatformServices {
],
);
}
if (Platform.isAndroid) {
// 다운로드 (yt-dlp · aria2) · 업데이트 · 탐색기 연결은 데스크톱 전용
MediaKitPreviewPlayer.ensureInitialized();
return PlatformServices(
mediaTool: FfmpegKitMediaTool(),
storage: AndroidStorageService(),
createPlayer: MediaKitPreviewPlayer.new,
createMediaPlayer: MediaKitFullPlayer.new,
createRecognizer: WhisperRecognizer.new,
createTranslator: NllbTranslator.new,
shell: AndroidShell(),
createSubtitleProviders: (s) => [
OpenSubtitlesProvider(
apiKey: () => s().openSubtitlesKey,
username: () => s().openSubtitlesUser,
password: () => s().openSubtitlesPassword,
),
],
);
}
throw UnsupportedError('아직 지원하지 않는 플랫폼입니다: ${Platform.operatingSystem}');
}
}

View File

@ -0,0 +1,207 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import '../platform/android/android_storage.dart';
import 'theme.dart';
/// 마지막으로 본 폴더 (다음에 열 때 여기서)
String? _lastDir;
/// Android 앱 안 파일 고르기: 폴더를 오가며 [extensions] 파일을 여러 개 고른다. 고른 파일의 실제 경로를 돌려준다.
Future<List<String>?> showAndroidFileBrowser(
BuildContext context, {
required String title,
required List<String> extensions,
String? initialDirectory,
}) =>
Navigator.of(context).push<List<String>>(MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => _FileBrowser(title: title, extensions: extensions, initialDirectory: initialDirectory),
));
class _FileBrowser extends StatefulWidget {
final String title;
final List<String> extensions;
final String? initialDirectory;
const _FileBrowser({required this.title, required this.extensions, this.initialDirectory});
@override
State<_FileBrowser> createState() => _FileBrowserState();
}
class _FileBrowserState extends State<_FileBrowser> {
String _root = '/storage/emulated/0';
String? _dir;
bool? _allowed;
List<Directory> _dirs = [];
List<File> _files = [];
final _picked = <String>{};
@override
void initState() {
super.initState();
_start();
}
Future<void> _start() async {
_root = await AndroidAccess.storageRoot();
final allowed = await AndroidAccess.hasAllFiles();
final first = [widget.initialDirectory, _lastDir, _root].firstWhere(
(d) => d != null && Directory(d).existsSync(),
orElse: () => _root)!;
setState(() => _allowed = allowed);
if (allowed) _open(first);
}
bool _match(String path) => widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase());
void _open(String dir) {
final dirs = <Directory>[];
final files = <File>[];
try {
for (final e in Directory(dir).listSync(followLinks: false)) {
final name = p.basename(e.path);
if (name.startsWith('.')) continue;
if (e is Directory) dirs.add(e);
if (e is File && _match(e.path)) files.add(e);
}
} catch (_) {}
int byName(FileSystemEntity a, FileSystemEntity b) =>
p.basename(a.path).toLowerCase().compareTo(p.basename(b.path).toLowerCase());
dirs.sort(byName);
files.sort(byName);
setState(() {
_dir = _lastDir = dir;
_dirs = dirs;
_files = files;
});
}
static String _size(int b) {
if (b >= 1 << 30) return '${(b / (1 << 30)).toStringAsFixed(1)}GB';
if (b >= 1 << 20) return '${(b / (1 << 20)).toStringAsFixed(1)}MB';
return '${(b / 1024).ceil()}KB';
}
@override
Widget build(BuildContext context) {
final dir = _dir;
final atRoot = dir == null || p.equals(dir, _root) || !p.isWithin(_root, dir);
return PopScope(
canPop: atRoot,
onPopInvokedWithResult: (didPop, _) {
if (!didPop && dir != null) _open(p.dirname(dir));
},
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
if (_files.isNotEmpty)
TextButton(
onPressed: () => setState(() {
final all = _files.every((f) => _picked.contains(f.path));
for (final f in _files) {
all ? _picked.remove(f.path) : _picked.add(f.path);
}
}),
child: const Text('이 폴더 전체'),
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilledButton(
onPressed: _picked.isEmpty ? null : () => Navigator.pop(context, _picked.toList()),
child: Text('추가 (${_picked.length})'),
),
),
],
),
body: switch (_allowed) {
null => const Center(child: CircularProgressIndicator()),
false => _permission(),
true => Column(children: [
// 바로 가기 · 지금 폴더
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
child: Row(children: [
for (final (label, sub) in [
('내장 저장소', ''),
('Download', 'Download'),
('Movies', 'Movies'),
('DCIM', 'DCIM'),
('jj_mkv', 'Movies/jj_mkv'),
])
if (Directory(p.join(_root, sub)).existsSync())
Padding(
padding: const EdgeInsets.only(right: 6),
child: ActionChip(label: Text(label), onPressed: () => _open(p.join(_root, sub))),
),
]),
),
ListTile(
dense: true,
leading: IconButton(
tooltip: '위 폴더',
icon: const Icon(Icons.arrow_upward),
onPressed: atRoot ? null : () => _open(p.dirname(dir)),
),
title: Text(
dir == null ? '' : (p.equals(dir, _root) ? '내장 저장소' : p.relative(dir, from: _root)),
style: const TextStyle(color: JjColors.textDim),
),
),
const Divider(height: 1),
Expanded(
child: _dirs.isEmpty && _files.isEmpty
? const Center(child: Text('이 폴더에는 고를 파일이 없습니다', style: TextStyle(color: JjColors.textDim)))
: ListView(children: [
for (final d in _dirs)
ListTile(
leading: const Icon(Icons.folder, color: Colors.amber),
title: Text(p.basename(d.path)),
onTap: () => _open(d.path),
),
for (final f in _files)
CheckboxListTile(
value: _picked.contains(f.path),
onChanged: (v) => setState(() => v! ? _picked.add(f.path) : _picked.remove(f.path)),
secondary: const Icon(Icons.movie_outlined),
title: Text(p.basename(f.path)),
subtitle: Text(_sizeOf(f)),
),
]),
),
]),
},
),
);
}
String _sizeOf(File f) {
try {
return _size(f.lengthSync());
} catch (_) {
return '';
}
}
Widget _permission() => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.folder_off_outlined, size: 48),
const SizedBox(height: 12),
const Text(
'동영상을 고르고, 동영상 옆 jj_mkv 폴더에 MKV 를 만들려면\n"모든 파일에 대한 접근" 권한이 필요합니다.',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton(onPressed: AndroidAccess.request, child: const Text('권한 허용 화면 열기')),
const SizedBox(height: 8),
TextButton(onPressed: _start, child: const Text('허용했으면 다시 확인')),
]),
),
);
}

View File

@ -240,6 +240,8 @@ class _SettingsPageState extends State<SettingsPage> {
listenable: c,
builder: (context, _) {
final s = c.settings;
// Android: 창 · 트레이 · 탐색기 · 다운로드 (yt-dlp · aria2) · 내장 Chrome 설정은 없음
final desk = !Platform.isAndroid;
return Scaffold(
body: Column(children: [
Container(
@ -298,6 +300,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
_textTile('홈 주소', s.homeUrl, '웹 브라우저의 시작 · 🏠 주소',
(v) => c.updateSettings((x) => x.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())),
if (desk) ...[
ListTile(
title: const Text('브라우저 엔진 (앱 안)'),
subtitle: Text(
@ -356,6 +359,7 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (v) => c.updateSettings((x) => x.externalBrowser = v!),
),
),
],
_section('저장 위치'),
_folderTile(
title: 'MKV · 자막 (jj_mkv)',
@ -367,6 +371,7 @@ class _SettingsPageState extends State<SettingsPage> {
},
onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null),
),
if (desk) ...[
_folderTile(
title: '다운로드 (jj_yt-dlp · jj_aria2)',
value: s.downloadRoot,
@ -384,6 +389,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: const Text('복사(Ctrl+C)한 주소 자동 다운로드'),
subtitle: const Text('YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)'),
),
],
_section('재생'),
ListTile(
title: const Text('동영상 하나를 재생할 때'),
@ -396,6 +402,7 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (m) => c.updateSettings((x) => x.playlistMode = m!),
),
),
if (desk) ...[
ListTile(
title: const Text('탐색기에서 동영상을 열 때'),
subtitle: const Text('더블클릭 · 연결 프로그램으로 JJ_MKVMaker 를 골랐을 때 (오른쪽 클릭 메뉴는 그대로)'),
@ -422,6 +429,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
_ExternalPlayers(c: c),
_ContextMenuTile(c: c),
],
_section('인터넷 자막 (OpenSubtitles.com)'),
_textTile('API 키 (필수, 무료)', s.openSubtitlesKey,
'가입 → 프로필 → API consumers → New consumer',
@ -430,6 +438,7 @@ class _SettingsPageState extends State<SettingsPage> {
(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 로 저장'),
@ -502,6 +511,7 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (n) => c.updateSettings((x) => x.maxParallelDownloads = n),
),
),
],
_section('변환'),
ListTile(
title: const Text('동시 MKV 변환 수'),
@ -511,6 +521,7 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (n) => c.updateSettings((x) => x.maxParallelJobs = n),
),
),
if (desk) ...[
_section('실행'),
ListTile(
title: const Text('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면'),
@ -560,6 +571,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: const Text('완료시 자동 동영상추가'),
subtitle: const Text('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)'),
),
],
_section('AI 자막'),
SwitchListTile(
value: s.askAiOptions,
@ -587,6 +599,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: const Text('시작할 때 새 버전 확인 (하루 한 번)'),
),
],
if (desk) ...[
ListTile(
title: const Text('필수 프로그램 점검'),
subtitle: const Text('FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다'),
@ -595,17 +608,18 @@ class _SettingsPageState extends State<SettingsPage> {
child: const Text('점검'),
),
),
],
ListTile(
title: const Text('오픈 소스 라이선스'),
subtitle: const Text('포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)'),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
OutlinedButton(
if (desk) OutlinedButton(
onPressed: () => c.services.shell.openExternal('system', [
'${File(Platform.resolvedExecutable).parent.path}\\THIRD_PARTY_NOTICES.txt',
]),
child: const Text('고지 문서'),
),
const SizedBox(width: 6),
if (desk) const SizedBox(width: 6),
OutlinedButton(
onPressed: () => showLicensePage(context: context, applicationName: 'JJ_MKVMaker'),
child: const Text('패키지 라이선스'),

View File

@ -0,0 +1,107 @@
## 2.6.2
* **Fixed: Windows build failure `Cannot extract through symlink ... .plugin_symlinks` (#165).** The prebuilt FFmpeg archive was unpacked into the plugin's own source directory. For a package resolved from the pub cache that directory is reached through a symlink, and the extractor refuses to write through it. The archive now unpacks into the application's build directory instead.
* The same change stops the plugin writing ~19 MB of native libraries into the shared pub cache on every platform. `flutter clean` never removed those; to reclaim the space delete `<pub-cache>/hosted/pub.dev/<package>-<version>/{linux,windows}/ffmpeg-kit` or run `dart pub cache repair`. The archive is now fetched once per build configuration rather than once per machine.
## 2.6.1
* **Fixed: `FFmpegKitConfig.getPlatform()` returned `windows` on Linux.** The Linux native layer was reporting the wrong platform name, so any code branching on it took the Windows path. It now returns `linux`. The prebuilt libraries attached to the `8.1.2-<variant>` release have been rebuilt with the fix; nothing else changed.
## 2.6.0
* **Linux support (x86_64).** `flutter run -d linux` and `flutter build linux` now work. Prebuilt FFmpeg **8.1.2** libraries for the `min-gpl` variant are downloaded at build time, the same way the Windows bundle already worked; set `FFMPEGKIT_LOCAL_DIR` (env or CMake cache variable) to build against a self-built bundle instead.
* Linux builds additionally require **`libjson-glib-dev`** (runtime: `libjson-glib`) alongside Flutter's standard Linux prerequisites. It ships with GNOME, so most desktops already have the runtime.
## 2.5.5
* **Fixed: log/statistics/complete callbacks silently dropped in apps with a second `FlutterEngine`** (e.g. a Firebase Messaging background isolate), a 4.4.1+ regression (#163). FFmpegKit's global native callbacks are now registered once and dispatched to every attached engine that is actually listening, instead of being captured by whichever engine attached last. Background-isolate execution (#155) keeps working.
## 2.5.4
* **Fixed: App Store rejection `Invalid architecture ... arm64e slice with the ios 18.5 SDK` (#164).** The iOS device slices of the bundled FFmpeg XCFrameworks are now arm64 only, for both Swift Package Manager (regenerated binary targets + checksums) and CocoaPods (setup script). arm64e was never used by App Store apps, so nothing is lost.
* SPM only: the regenerated iOS/macOS binaries now actually include the zlib/PNG decoders (#105) and VideoToolbox hardware encoders (#148) that the previous release added for CocoaPods and Android but not for the SPM artifacts.
## 2.5.3
* **Fixed: `Decoder (codec png) not found` (#105).** The native FFmpeg builds for this variant were compiled without zlib, which silently removed the PNG/APNG decoders. zlib is now enabled on Android (`com.antonkarpenko:ffmpeg-kit-*:2.2.2`), iOS and macOS, restoring PNG/APNG decoding (image overlays etc.) as in the original arthenica builds.
## 2.5.2
* **iOS/macOS: hardware-accelerated H.264/HEVC encoding via Apple VideoToolbox** (`h264_videotoolbox`, `hevc_videotoolbox`), plus AVFoundation, is now enabled for this variant. Previously only the `full` / `full_gpl` variants shipped VideoToolbox. No patent-encumbered codecs are added and the codec surface is otherwise unchanged (#148).
## 2.5.1
* Fixed the iOS/macOS build error `'ffmpegkit/FFmpegKitConfig.h' file not found` (#88). The framework setup scripts now download and install atomically, verify the archive, and never leave an empty `Frameworks/` directory behind after a failed download; the podspec re-runs setup until the frameworks are actually present. On restricted networks, point `FFMPEG_KIT_IOS_URL` / `FFMPEG_KIT_MACOS_URL` at a mirror of the release zip.
* Docs: made the FFmpeg badge a link and tidied the README badges.
## 2.5.0
* **Swift Package Manager support** (iOS & macOS): the plugin now integrates via Flutter's SPM support (`flutter config --enable-swift-package-manager`; enabled by default from Flutter 3.44) as well as CocoaPods. Native FFmpeg frameworks are consumed as prebuilt, checksum-pinned XCFrameworks with a native arm64 iOS-simulator slice, downloaded from the GitHub release.
* iOS/macOS plugin sources moved to the Flutter SPM plugin layout; CocoaPods integration is unchanged (and still supported).
* Example app now includes runtime integration tests (`example/integration_test/`).
## 2.4.2
* Fixes the native binary wiring for the CVE-2026-8461 (MagicYUV / "PixelSmash") security patch: Android now pulls `com.antonkarpenko:ffmpeg-kit-*:2.2.1` and iOS/macOS fetch the FFmpeg 8.1.2 frameworks (the 8.1.2 rebuild was previously only wired for Windows). Please upgrade from any earlier version.
## 2.4.1
* **Security (CVE-2026-8461):** updated **FFmpeg to v8.1.2** (arthenica `n8.1.2`) to fix a heap out-of-bounds write in the MagicYUV decoder ("PixelSmash", CVSS 8.8). All platforms (Android/iOS/macOS/Windows) rebuilt against FFmpeg 8.1.2; no API changes.
## 2.4.0
* Updated **FFmpeg to v8.1.1** (arthenica `n8.1.1`), up from 8.0.0.
* iOS/macOS `.xcframework`s, the Android Maven library (`com.antonkarpenko:ffmpeg-kit-*`), and the Windows binaries are now built against FFmpeg 8.1.1.
## 2.3.2
* Android: broadened the bundled ProGuard/R8 consumer rules to keep **all** `com.antonkarpenko.ffmpegkit.**` classes (plus `-dontwarn`), not just the JNI entry points. Fully prevents R8 from stripping FFmpegKit in release builds — fixes the release-only white screen / `channel-error` (FFmpegKit failing to initialise, which cascades into errors like `shared_preferences` `getAll`). Closes #158.
## 2.3.1
* Android: ship ProGuard/R8 **consumer rules** (`consumer-rules.pro`, applied automatically via `consumerProguardFiles`) that keep the FFmpegKit JNI bindings — `FFmpegKitConfig` native/callback methods and `AbiDetect`. Prevents release-mode crashes such as `Bad JNI version returned from JNI_OnLoad` without requiring any ProGuard rules in your app. Thanks @niclasEX (#133).
## 2.3.0
* Fixed **iOS Simulator on Apple Silicon** (`arm64`) support — required for Xcode 26 / iOS 26+ simulators. Builds previously failed with *"The following target(s) do not support arm64 architecture, which is a requirement for Apple Silicon iOS 26+ simulators"*.
* The downloaded iOS `.framework` bundles are now converted to `.xcframework`s at build time (`scripts/setup_ios.sh`), exposing a native `ios-arm64_x86_64-simulator` slice alongside the `ios-arm64_arm64e` device slice. The simulator `arm64` slice is produced by retagging the device `arm64` slice's Mach-O build-version platform to `iOS-Simulator` via `vtool`.
* The iOS podspec now vendors `.xcframework`s and no longer excludes `arm64` for the simulator (`EXCLUDED_ARCHS[sdk=iphonesimulator*]` is now `i386` only).
* If you previously added `config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"` to your app's `ios/Podfile` `post_install` hook as a workaround, **remove it** — it now prevents the simulator `arm64` slice from linking.
## 2.2.0
* Added **Windows** support (x86_64) for the `min-gpl` FFmpeg 8.0 variant (bare FFmpeg plus the four GPL video libraries: `x264`, `x265`, `xvid`, `vid.stab`).
* Native libraries are downloaded automatically at build time from the `8.0.0-min-gpl` release (`ffmpeg-kit-windows-x86_64-min-gpl-8.0.0.zip`); for local development point `FFMPEGKIT_LOCAL_DIR` (CMake cache var or environment) at a locally built bundle.
* The bundled MinGW DLLs are rebased/ASLR-adjusted at build time and the full runtime dependency closure is shipped so the plugin loads standalone.
* Shell scripts are forced to LF via `.gitattributes` so the published package never carries Windows CRLF line endings (avoids the iOS/macOS `pod install` `/bin/bash^M: bad interpreter` failure).
## 2.1.1
* Added ProGuard rules to support release builds
* Enabled ProGuard for Android release builds
## 2.1.0
* Fixed the FFmpeg 8.0 compatibility issue across all platforms. The problem was that `all_channel_counts` was being set AFTER the filter was created, but FFmpeg 8.0 requires it to be set DURING filter creation.
## 2.0.0
* FFmpeg `v8.0.0` with [all the sweet perks](https://ffmpeg.org/index.html#news)
## 1.1.0
* Added proguard-rules.pro to keep `ffmpeg` dependencies when minification is enabled
* Upgraded `freetype` from **2.13.0** to **2.13.3**
* Upgraded `harfbuzz` from **8.0.1** to **11.3.3**
* Upgraded `fontconfig` from **2.16.2** to 2.17.1
* Added support for `harfbuzz` library in order to support `drawtext` filter
* Fixed missing `libunibreak` for `libass.sh`
* Downgraded required Kotlin version to `v1.8.22`
* Upgraded com.android.library from `8.11.1` to `8.12.0`
## 1.0.0
* Initial release
* FFmpeg version 7.1.1
* Removed bundled Android FFmpeg (jniLibs, cpp, bindings)
* Added FFmpeg min using new Maven Central package

View File

@ -0,0 +1,11 @@
# ffmpeg_kit_flutter_new_min_gpl 2.6.2 사본 (JJ_MKVMaker 패치)
원본: https://pub.dev/packages/ffmpeg_kit_flutter_new_min_gpl 2.6.2 (pubspec.yaml 의 dependency_overrides 로 사용)
## 고친 문제
원본은 Windows 에도 FFmpeg DLL (약 30MB) 을 프로그램 폴더에 넣는다.
JJ_MKVMaker 의 Windows 판은 동봉한 ffmpeg.exe 를 쓰므로 필요 없다.
## 수정
- pubspec.yaml: 플러그인 플랫폼을 android 만 남김 (ios · macos · windows · linux 제거)
- windows · linux · macos · ios · example 폴더 제거

View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -0,0 +1,282 @@
<div align="center">
# 🎬 FFmpegKit for Flutter — Minimal-GPL
**Run `FFmpeg` & `FFprobe` on Android, iOS, macOS, Windows and Linux from a single Dart API.**
_A maintained fork of the original [FFmpegKit](https://github.com/arthenica/ffmpeg-kit/tree/main/flutter/flutter), updated for the Android V2 embedding and Flutter 3+._
<p align="center">
<a href="https://pub.dev/packages/ffmpeg_kit_flutter_new_min_gpl"><img src="https://img.shields.io/badge/pub-2.5.5-blue?logo=dart" alt="pub version"></a>
<a href="https://ffmpeg.org/index.html#news"><img src="https://img.shields.io/badge/FFmpeg-8.1.2-green?logo=ffmpeg&logoColor=white" alt="FFmpeg 8.1.2"></a>
<a href="https://discord.gg/8NVwykjA"><img src="https://img.shields.io/discord/1387108888452665427?logo=discord&logoColor=white&label=Join+Us&color=blueviolet" alt="Discord"></a>
<a href="https://buymeacoffee.com/sk3llo" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="21" width="114"></a>
</p>
</div>
---
## ✨ Features
- 🧰 **Full toolkit** — both `FFmpeg` and `FFprobe`, with per-session logs, statistics and callbacks.
- 📱 **Five platforms** — `Android`, `iOS`, `macOS`, `Windows` (x86_64), and `Linux` (x86_64) behind one API.
- 🎞️ **FFmpeg `v8.1.2`** — full command-line-compatible build.
- 🗂️ **Android SAF** — process Storage Access Framework URIs directly.
- 📚 **External libraries** — see the [enabled-libraries table](#-enabled-libraries) below.
- 🔄 **Modernised bindings** for the latest Flutter and Android/macOS toolchains.
- 📦 **Swift Package Manager** — iOS & macOS integrate via SPM (Flutter 3.24+) or CocoaPods, with prebuilt checksum-pinned XCFrameworks.
## 📦 Install
```yaml
dependencies:
ffmpeg_kit_flutter_new_min_gpl: ^2.6.0
```
```dart
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart';
```
## 🎯 Choose your package
There are **eight** `ffmpeg-kit` packages — pick the smallest one that covers the codecs and features you need. `GPL`-licensed codecs (`x264`, `x265`, `xvidcore`, `vid.stab`) are only in the `-gpl` packages.
| Package | Best for |
|---|---|
| [`_min`](https://pub.dev/packages/ffmpeg_kit_flutter_new_min) | Smallest build — core FFmpeg only |
| [`_min_gpl`](https://pub.dev/packages/ffmpeg_kit_flutter_new_min_gpl) **(this one)** | Minimal **+ GPL** codecs (x264/x265/xvid/vid.stab) |
| [`_https`](https://pub.dev/packages/ffmpeg_kit_flutter_new_https) | Adds TLS (`gnutls`) for `https://` inputs |
| [`_https_gpl`](https://pub.dev/packages/ffmpeg_kit_flutter_new_https_gpl) | HTTPS **+ GPL** codecs |
| [`_audio`](https://pub.dev/packages/ffmpeg_kit_flutter_new_audio) | Audio-focused (mp3, opus, vorbis, speex, …) |
| [`_video`](https://pub.dev/packages/ffmpeg_kit_flutter_new_video) | Video-focused (dav1d, vpx, theora, webp, …) |
| [`_full`](https://pub.dev/packages/ffmpeg_kit_flutter_new_full) | Everything except GPL codecs |
| [`ffmpeg_kit_flutter_new`](https://pub.dev/packages/ffmpeg_kit_flutter_new) | **Full + GPL** — every library |
## 🚀 Quick start
```dart
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart';
final session = await FFmpegKit.execute('-i input.mp4 -c:v mpeg4 output.mp4');
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
// ✅ done
} else if (ReturnCode.isCancel(returnCode)) {
// ⏹️ cancelled
} else {
// ❌ error — inspect await session.getLogs()
}
```
## 🧩 Enabled libraries
Below is which system and external libraries each package enables. Some parts of `FFmpeg` are `GPL`-licensed and are only present in the `GPL` packages.
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"><sup>min</sup></th>
<th align="center"><sup>min-gpl</sup></th>
<th align="center"><sup>https</sup></th>
<th align="center"><sup>https-gpl</sup></th>
<th align="center"><sup>audio</sup></th>
<th align="center"><sup>video</sup></th>
<th align="center"><sup>full</sup></th>
<th align="center"><sup>full-gpl</sup></th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><sup>external libraries</sup></td>
<td align="center">-</td>
<td align="center"><sup>vid.stab</sup><br><sup>x264</sup><br><sup>x265</sup><br><sup>xvidcore</sup></td>
<td align="center"><sup>gmp</sup><br><sup>gnutls</sup></td>
<td align="center"><sup>gmp</sup><br><sup>gnutls</sup><br><sup>vid.stab</sup><br><sup>x264</sup><br><sup>x265</sup><br><sup>xvidcore</sup></td>
<td align="center"><sup>lame</sup><br><sup>libilbc</sup><br><sup>libvorbis</sup><br><sup>opencore-amr</sup><br><sup>opus</sup><br><sup>shine</sup><br><sup>soxr</sup><br><sup>speex</sup><br><sup>twolame</sup><br><sup>vo-amrwbenc</sup></td>
<td align="center"><sup>dav1d</sup><br><sup>fontconfig</sup><br><sup>freetype</sup><br><sup>fribidi</sup><br><sup>kvazaar</sup><br><sup>libass</sup><br><sup>libiconv</sup><br><sup>libtheora</sup><br><sup>libvpx</sup><br><sup>libwebp</sup><br><sup>snappy</sup><br><sup>zimg</sup></td>
<td align="center"><sup>dav1d</sup><br><sup>fontconfig</sup><br><sup>freetype</sup><br><sup>fribidi</sup><br><sup>gmp</sup><br><sup>gnutls</sup><br><sup>kvazaar</sup><br><sup>lame</sup><br><sup>libass</sup><br><sup>libiconv</sup><br><sup>libilbc</sup><br><sup>libtheora</sup><br><sup>libvorbis</sup><br><sup>libvpx</sup><br><sup>libwebp</sup><br><sup>libxml2</sup><br><sup>opencore-amr</sup><br><sup>opus</sup><br><sup>shine</sup><br><sup>snappy</sup><br><sup>soxr</sup><br><sup>speex</sup><br><sup>twolame</sup><br><sup>vo-amrwbenc</sup><br><sup>zimg</sup></td>
<td align="center"><sup>dav1d</sup><br><sup>fontconfig</sup><br><sup>freetype</sup><br><sup>fribidi</sup><br><sup>gmp</sup><br><sup>gnutls</sup><br><sup>kvazaar</sup><br><sup>lame</sup><br><sup>libass</sup><br><sup>libiconv</sup><br><sup>libilbc</sup><br><sup>libtheora</sup><br><sup>libvorbis</sup><br><sup>libvpx</sup><br><sup>libwebp</sup><br><sup>libxml2</sup><br><sup>opencore-amr</sup><br><sup>opus</sup><br><sup>shine</sup><br><sup>snappy</sup><br><sup>soxr</sup><br><sup>speex</sup><br><sup>twolame</sup><br><sup>vid.stab</sup><br><sup>vo-amrwbenc</sup><br><sup>x264</sup><br><sup>x265</sup><br><sup>xvidcore</sup><br><sup>zimg</sup></td>
</tr>
<tr>
<td align="center"><sup>android system libraries</sup></td>
<td align="center" colspan=8><sup>zlib</sup><br><sup>MediaCodec</sup></td>
</tr>
<tr>
<td align="center"><sup>ios system libraries</sup></td>
<td align="center" colspan=8><sup>bzip2</sup><br><sup>AudioToolbox</sup><br><sup>AVFoundation</sup><br><sup>iconv</sup><br><sup>zlib</sup></td>
</tr>
<tr>
<td align="center"><sup>ios VideoToolbox</sup></td>
<td align="center"><sup>VideoToolbox</sup></td>
<td align="center"><sup>VideoToolbox</sup></td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center">-</td>
<td align="center"><sup>VideoToolbox</sup></td>
<td align="center"><sup>VideoToolbox</sup></td>
</tr>
<tr>
<td align="center"><sup>macos system libraries</sup></td>
<td align="center" colspan=8><sup>bzip2</sup><br><sup>AudioToolbox</sup><br><sup>AVFoundation</sup><br><sup>Core Image</sup><br><sup>iconv</sup><br><sup>OpenCL</sup><br><sup>OpenGL</sup><br><sup>VideoToolbox</sup><br><sup>zlib</sup></td>
</tr>
</tbody>
</table>
## 📱 Platform support
<table align="center">
<thead>
<tr>
<th align="center">Android<br>API Level</th>
<th align="center">Kotlin<br>Min Version</th>
<th align="center">iOS<br>Min Target</th>
<th align="center">macOS<br>Min Target</th>
<th align="center">Windows</th>
<th align="center">Linux</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">24</td>
<td align="center">1.8.22</td>
<td align="center">14.0</td>
<td align="center">10.15</td>
<td align="center">10+ (x86_64)</td>
<td align="center">x86_64</td>
</tr>
</tbody>
</table>
**Architectures** — Android: `arm-v7a`, `arm-v7a-neon`, `arm64-v8a`, `x86`, `x86_64` · iOS: `arm64` (device) + `arm64`/`x86_64` (simulator, incl. Apple Silicon / Xcode 26), shipped as `.xcframework` · macOS: `arm64`, `x86_64`.
> **Windows:** prebuilt FFmpeg **8.1.2** libraries (x86_64) are downloaded automatically at build time. For local development against a self-built bundle, set `FFMPEGKIT_LOCAL_DIR` (env or CMake cache variable) to the bundle directory before `flutter run/build windows`.
> **Linux:** prebuilt FFmpeg **8.1.2** libraries (x86_64) are downloaded automatically at build time, exactly as on Windows; `FFMPEGKIT_LOCAL_DIR` works the same way. In addition to Flutter's standard Linux prerequisites (`clang`, `cmake`, `ninja-build`, `pkg-config`, `libgtk-3-dev`), the plugin needs **`libjson-glib-dev`**:
>
> ```bash
> sudo apt-get install libjson-glib-dev # Debian/Ubuntu
> sudo dnf install json-glib-devel # Fedora
> ```
## 📖 Usage
<details open>
<summary><strong>Execute a command & read the result</strong></summary>
```dart
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart';
FFmpegKit.execute('-i file1.mp4 -c:v mpeg4 file2.mp4').then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
// SUCCESS
} else if (ReturnCode.isCancel(returnCode)) {
// CANCEL
} else {
// ERROR
}
});
```
</details>
<details>
<summary><strong>Inspect a session</strong></summary>
```dart
FFmpegKit.execute('-i file1.mp4 -c:v mpeg4 file2.mp4').then((session) async {
final sessionId = session.getSessionId();
final command = session.getCommand();
final state = await session.getState();
final returnCode = await session.getReturnCode();
final duration = await session.getDuration();
final output = await session.getOutput();
final logs = await session.getLogs();
final statistics = await (session as FFmpegSession).getStatistics();
});
```
</details>
<details>
<summary><strong>Async execution with callbacks</strong></summary>
```dart
FFmpegKit.executeAsync('-i file1.mp4 -c:v mpeg4 file2.mp4', (Session session) async {
// CALLED WHEN SESSION IS EXECUTED
}, (Log log) {
// CALLED WHEN SESSION PRINTS LOGS
}, (Statistics statistics) {
// CALLED WHEN SESSION GENERATES STATISTICS
});
```
</details>
<details>
<summary><strong>FFprobe & media information</strong></summary>
```dart
FFprobeKit.getMediaInformation('<file path or url>').then((session) async {
final information = await session.getMediaInformation();
});
```
</details>
<details>
<summary><strong>Cancel sessions</strong></summary>
```dart
FFmpegKit.cancel(); // stop all sessions
FFmpegKit.cancel(sessionId); // stop a specific session
```
</details>
<details>
<summary><strong>Android — Storage Access Framework (SAF)</strong></summary>
```dart
// Reading a file
FFmpegKitConfig.selectDocumentForRead('*/*').then((uri) {
FFmpegKitConfig.getSafParameterForRead(uri!).then((safUrl) {
FFmpegKit.executeAsync("-i ${safUrl!} -c:v mpeg4 file2.mp4");
});
});
// Writing to a file
FFmpegKitConfig.selectDocumentForWrite('video.mp4', 'video/*').then((uri) {
FFmpegKitConfig.getSafParameterForWrite(uri!).then((safUrl) {
FFmpegKit.executeAsync("-i file1.mp4 -c:v mpeg4 ${safUrl}");
});
});
```
</details>
<details>
<summary><strong>Global callbacks & fonts</strong></summary>
```dart
FFmpegKitConfig.enableLogCallback((log) { final message = log.getMessage(); });
FFmpegKitConfig.enableStatisticsCallback((statistics) { final size = statistics.getSize(); });
FFmpegKitConfig.setFontDirectoryList(["/system/fonts", "/System/Library/Fonts", "<folder with fonts>"]);
```
</details>
## 📄 License
Licensed under **LGPL 3.0** by default. This package additionally includes `GPL v3.0`-licensed components (`x264`, `x265`, `xvidcore`, `vid.stab`) and is therefore effectively **GPL v3.0**.
## 💬 Community & support
- 💙 [Join the Discord](https://discord.gg/8NVwykjA)
- ☕ [Buy me a coffee](https://buymeacoffee.com/sk3llo)
- 🐛 [Report an issue](https://github.com/sk3llo/ffmpeg_kit_flutter/issues)

View File

@ -0,0 +1,63 @@
analyzer:
strong-mode:
implicit-dynamic: false
errors:
missing_required_param: warning
missing_return: warning
linter:
rules:
- avoid_catching_errors
- avoid_function_literals_in_foreach_calls
- avoid_private_typedef_functions
- avoid_renaming_method_parameters
- avoid_returning_null_for_void
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
- camel_case_types
- cancel_subscriptions
- comment_references
- constant_identifier_names
- control_flow_in_finally
- directives_ordering
- empty_statements
- file_names
- hash_and_equals
- implementation_imports
- join_return_with_assignment
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- no_runtimeType_toString
- non_constant_identifier_names
- only_throw_errors
- overridden_fields
- package_names
- package_prefixed_library_names
- prefer_asserts_in_initializer_lists
- prefer_const_constructors
- prefer_const_declarations
- prefer_expression_function_bodies
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- sort_pub_dependencies
- test_types_in_equals
- throw_in_finally
- unnecessary_brace_in_string_interps
- unnecessary_lambdas
- unnecessary_null_aware_assignments
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_interpolations
- use_string_buffers
- void_checks

View File

@ -0,0 +1,56 @@
buildscript {
ext.kotlin_version = "2.2.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.antonkarpenko.ffmpegkit"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
buildTypes {
release {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
defaultConfig {
minSdk = 24
versionCode 1
versionName "1.0.0"
consumerProguardFiles 'consumer-rules.pro'
}
dependencies {
implementation("androidx.annotation:annotation:1.9.1")
implementation("com.antonkarpenko:ffmpeg-kit-min-gpl:2.2.2")
}
}

View File

@ -0,0 +1,17 @@
# Keep all FFmpegKit classes (incl. JNI entry points and the Flutter plugin) so
# R8/ProGuard never strips them in release builds. Stripping causes native-load
# failures that surface as a white screen / plugin-registration channel errors.
-keep class com.antonkarpenko.ffmpegkit.** { *; }
-dontwarn com.antonkarpenko.ffmpegkit.**
-keep class com.antonkarpenko.ffmpegkit.FFmpegKitConfig {
native <methods>;
void log(long, int, byte[]);
void statistics(long, int, float, float, long , double, double, double);
int safOpen(int);
int safClose(int);
}
-keep class com.antonkarpenko.ffmpegkit.AbiDetect {
native <methods>;
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,51 @@
# Flutter framework and engine (core retention rules)
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**
-ignorewarnings
-keep class io.flutter.embedding.engine.plugins.** { *; }
-keep class io.flutter.embedding.engine.renderer.** { *; }
-keep class io.flutter.embedding.engine.systemchannels.** { *; }
# FFmpegKit rules
-keep class com.antonkarpenko.ffmpegkit.** { *; }
-dontwarn com.antonkarpenko.ffmpegkit.**
# Keep all FFmpegKit native methods
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep FFmpegKit Config
-keep class com.antonkarpenko.ffmpegkit.FFmpegKitConfig {
*;
}
# Keep ABI Detection
-keep class com.antonkarpenko.ffmpegkit.AbiDetect {
*;
}
# Keep all FFmpegKit sessions
-keep class com.antonkarpenko.ffmpegkit.*Session {
*;
}
# Keep FFmpegKit callbacks
-keep class com.antonkarpenko.ffmpegkit.*Callback {
*;
}
# Preserve all public classes in ffmpegkit
-keep public class com.antonkarpenko.ffmpegkit.** {
public *;
}
# Keep reflection-based access
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

View File

@ -0,0 +1,2 @@
rootProject.name = 'ffmpeg-kit-flutter-android'

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.antonkarpenko.ffmpegkit">
</manifest>

View File

@ -0,0 +1,63 @@
package com.antonkarpenko.ffmpegkit;
import static com.antonkarpenko.ffmpegkit.FFmpegKitFlutterPlugin.LIBRARY_NAME;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
public class FFmpegKitFlutterMethodResultHandler {
private final Handler handler;
FFmpegKitFlutterMethodResultHandler() {
handler = new Handler(Looper.getMainLooper());
}
public void successAsync(final MethodChannel.Result result, final Object object) {
handler.post(() -> {
if (result != null) {
result.success(object);
} else {
Log.w(LIBRARY_NAME, String.format("ResultHandler can not send successful response %s on a null method call result.", object));
}
});
}
void successAsync(final EventChannel.EventSink eventSink, final Object object) {
handler.post(() -> {
if (eventSink != null) {
eventSink.success(object);
} else {
Log.w(LIBRARY_NAME, String.format("ResultHandler can not send event %s on a null event sink.", object));
}
});
}
void errorAsync(final MethodChannel.Result result, final String errorCode, final String errorMessage) {
errorAsync(result, errorCode, errorMessage, null);
}
void errorAsync(final MethodChannel.Result result, final String errorCode, final String errorMessage, final Object errorDetails) {
handler.post(() -> {
if (result != null) {
result.error(errorCode, errorMessage, errorDetails);
} else {
Log.w(LIBRARY_NAME, String.format("ResultHandler can not send failure response %s:%s on a null method call result.", errorCode, errorMessage));
}
});
}
void notImplementedAsync(final MethodChannel.Result result) {
handler.post(() -> {
if (result != null) {
result.notImplemented();
} else {
Log.w(LIBRARY_NAME, "ResultHandler can not send not implemented response on a null method call result.");
}
});
}
}

View File

@ -0,0 +1,25 @@
package com.antonkarpenko.ffmpegkit;
import androidx.annotation.NonNull;
import com.antonkarpenko.ffmpegkit.FFmpegKitConfig;
import io.flutter.plugin.common.MethodChannel;
public class FFmpegSessionExecuteTask implements Runnable {
private final com.antonkarpenko.ffmpegkit.FFmpegSession ffmpegSession;
private final FFmpegKitFlutterMethodResultHandler resultHandler;
private final MethodChannel.Result result;
public FFmpegSessionExecuteTask(@NonNull final com.antonkarpenko.ffmpegkit.FFmpegSession ffmpegSession, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) {
this.ffmpegSession = ffmpegSession;
this.resultHandler = resultHandler;
this.result = result;
}
@Override
public void run() {
FFmpegKitConfig.ffmpegExecute(ffmpegSession);
resultHandler.successAsync(result, null);
}
}

View File

@ -0,0 +1,26 @@
package com.antonkarpenko.ffmpegkit;
import androidx.annotation.NonNull;
import com.antonkarpenko.ffmpegkit.FFmpegKitConfig;
import com.antonkarpenko.ffmpegkit.FFprobeSession;
import io.flutter.plugin.common.MethodChannel;
public class FFprobeSessionExecuteTask implements Runnable {
private final FFprobeSession ffprobeSession;
private final FFmpegKitFlutterMethodResultHandler resultHandler;
private final MethodChannel.Result result;
public FFprobeSessionExecuteTask(@NonNull final FFprobeSession ffprobeSession, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) {
this.ffprobeSession = ffprobeSession;
this.resultHandler = resultHandler;
this.result = result;
}
@Override
public void run() {
FFmpegKitConfig.ffprobeExecute(ffprobeSession);
resultHandler.successAsync(result, null);
}
}

View File

@ -0,0 +1,28 @@
package com.antonkarpenko.ffmpegkit;
import androidx.annotation.NonNull;
import com.antonkarpenko.ffmpegkit.FFmpegKitConfig;
import com.antonkarpenko.ffmpegkit.MediaInformationSession;
import io.flutter.plugin.common.MethodChannel;
public class MediaInformationSessionExecuteTask implements Runnable {
private final MediaInformationSession mediaInformationSession;
private final int timeout;
private final FFmpegKitFlutterMethodResultHandler resultHandler;
private final MethodChannel.Result result;
public MediaInformationSessionExecuteTask(@NonNull final MediaInformationSession mediaInformationSession, final int timeout, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, final MethodChannel.Result result) {
this.mediaInformationSession = mediaInformationSession;
this.timeout = timeout;
this.resultHandler = resultHandler;
this.result = result;
}
@Override
public void run() {
FFmpegKitConfig.getMediaInformationExecute(mediaInformationSession, timeout);
resultHandler.successAsync(result, null);
}
}

View File

@ -0,0 +1,51 @@
package com.antonkarpenko.ffmpegkit;
import static com.antonkarpenko.ffmpegkit.FFmpegKitFlutterPlugin.LIBRARY_NAME;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.IOException;
import io.flutter.plugin.common.MethodChannel;
public class WriteToPipeTask implements Runnable {
private final String inputPath;
private final String namedPipePath;
private final FFmpegKitFlutterMethodResultHandler resultHandler;
private final MethodChannel.Result result;
public WriteToPipeTask(@NonNull final String inputPath, @NonNull final String namedPipePath, @NonNull final FFmpegKitFlutterMethodResultHandler resultHandler, @NonNull final MethodChannel.Result result) {
this.inputPath = inputPath;
this.namedPipePath = namedPipePath;
this.resultHandler = resultHandler;
this.result = result;
}
@Override
public void run() {
final int rc;
try {
final String asyncCommand = "cat " + inputPath + " > " + namedPipePath;
Log.d(LIBRARY_NAME, String.format("Starting copy %s to pipe %s operation.", inputPath, namedPipePath));
final long startTime = System.currentTimeMillis();
final Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", asyncCommand});
rc = process.waitFor();
final long endTime = System.currentTimeMillis();
Log.d(LIBRARY_NAME, String.format("Copying %s to pipe %s operation completed with rc %d in %d seconds.", inputPath, namedPipePath, rc, (endTime - startTime) / 1000));
resultHandler.successAsync(result, rc);
} catch (final IOException | InterruptedException e) {
Log.e(LIBRARY_NAME, String.format("Copy %s to pipe %s failed with error.", inputPath, namedPipePath), e);
resultHandler.errorAsync(result, "WRITE_TO_PIPE_FAILED", e.getMessage());
}
}
}

View File

@ -0,0 +1,445 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
import 'ffmpeg_session.dart';
import 'ffprobe_session.dart';
import 'log.dart';
import 'log_callback.dart';
import 'log_redirection_strategy.dart';
import 'media_information.dart';
import 'media_information_session.dart';
import 'return_code.dart';
import 'session.dart';
import 'session_state.dart';
import 'src/ffmpeg_kit_factory.dart';
/// Abstract session implementation which includes common features shared by
/// "FFmpeg", "FFprobe" and "MediaInformation" sessions.
abstract class AbstractSession extends Session {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Defines how long default "getAll" methods wait, in milliseconds.
static const defaultTimeoutForAsynchronousMessagesInTransmit = 5000;
/// Session identifier.
int? _sessionId;
/// Date and time the session was created.
DateTime? _createTime;
/// Date and time the session was started.
DateTime? _startTime;
/// Command string.
String? _command;
/// Command arguments as an array.
List<String>? _argumentsArray;
/// Session specific log redirection strategy.
LogRedirectionStrategy? _logRedirectionStrategy;
/// Creates a new FFmpeg session using [argumentsArray] and
/// [logRedirectionStrategy].
///
/// Returns FFmpeg session created.
static Future<FFmpegSession> createFFmpegSession(List<String> argumentsArray,
[LogRedirectionStrategy? logRedirectionStrategy = null]) async {
try {
await FFmpegKitConfig.init();
final Map<dynamic, dynamic>? nativeSession =
await _platform.abstractSessionCreateFFmpegSession(argumentsArray);
final session = new FFmpegSession();
session._sessionId = nativeSession?["sessionId"];
session._createTime =
FFmpegKitFactory.validDate(nativeSession?["createTime"]);
session._startTime =
FFmpegKitFactory.validDate(nativeSession?["startTime"]);
session._command = nativeSession?["command"];
session._argumentsArray = argumentsArray;
session._logRedirectionStrategy =
logRedirectionStrategy ?? FFmpegKitConfig.getLogRedirectionStrategy();
FFmpegKitFactory.setLogRedirectionStrategy(
session._sessionId, logRedirectionStrategy);
return session;
} on PlatformException catch (e, stack) {
print("Plugin createFFmpegSession error: ${e.message}");
return Future.error("createFFmpegSession failed.", stack);
}
}
/// Creates a new FFmpeg session from [sessionMap], which includes session
/// fields as map keys.
///
/// Returns FFmpeg session created.
static FFmpegSession createFFmpegSessionFromMap(
Map<dynamic, dynamic> sessionMap) {
final session = new FFmpegSession();
session._sessionId = sessionMap["sessionId"];
session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]);
session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]);
session._command = sessionMap["command"];
session._argumentsArray =
FFmpegKitConfig.parseArguments(sessionMap["command"]);
session._logRedirectionStrategy =
FFmpegKitFactory.getLogRedirectionStrategy(session._sessionId);
return session;
}
/// Creates a new FFprobe session using [argumentsArray] and
/// [logRedirectionStrategy].
///
/// Returns FFprobe session created.
static Future<FFprobeSession> createFFprobeSession(
List<String> argumentsArray,
[LogRedirectionStrategy? logRedirectionStrategy = null]) async {
try {
await FFmpegKitConfig.init();
final Map<dynamic, dynamic>? nativeSession =
await _platform.abstractSessionCreateFFprobeSession(argumentsArray);
final session = new FFprobeSession();
session._sessionId = nativeSession?["sessionId"];
session._createTime =
FFmpegKitFactory.validDate(nativeSession?["createTime"]);
session._startTime =
FFmpegKitFactory.validDate(nativeSession?["startTime"]);
session._command = nativeSession?["command"];
session._argumentsArray = argumentsArray;
session._logRedirectionStrategy =
logRedirectionStrategy ?? FFmpegKitConfig.getLogRedirectionStrategy();
FFmpegKitFactory.setLogRedirectionStrategy(
session._sessionId, logRedirectionStrategy);
return session;
} on PlatformException catch (e, stack) {
print("Plugin createFFprobeSession error: ${e.message}");
return Future.error("createFFprobeSession failed.", stack);
}
}
/// Creates a new FFprobe session from [sessionMap], which includes session
/// fields as map keys.
///
/// Returns FFprobe session created.
static FFprobeSession createFFprobeSessionFromMap(
Map<dynamic, dynamic> sessionMap) {
final session = new FFprobeSession();
session._sessionId = sessionMap["sessionId"];
session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]);
session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]);
session._command = sessionMap["command"];
session._argumentsArray =
FFmpegKitConfig.parseArguments(sessionMap["command"]);
session._logRedirectionStrategy =
FFmpegKitFactory.getLogRedirectionStrategy(session._sessionId);
return session;
}
/// Creates a new MediaInformation session using [argumentsArray].
///
/// Returns MediaInformation session created.
static Future<MediaInformationSession> createMediaInformationSession(
List<String> argumentsArray) async {
try {
await FFmpegKitConfig.init();
final Map<dynamic, dynamic>? nativeSession = await _platform
.abstractSessionCreateMediaInformationSession(argumentsArray);
final session = new MediaInformationSession();
session._sessionId = nativeSession?["sessionId"];
session._createTime =
FFmpegKitFactory.validDate(nativeSession?["createTime"]);
session._startTime =
FFmpegKitFactory.validDate(nativeSession?["startTime"]);
session._command = nativeSession?["command"];
session._argumentsArray = argumentsArray;
session._logRedirectionStrategy = LogRedirectionStrategy.neverPrintLogs;
FFmpegKitFactory.setLogRedirectionStrategy(
session._sessionId, LogRedirectionStrategy.neverPrintLogs);
return session;
} on PlatformException catch (e, stack) {
print("Plugin createMediaInformationSession error: ${e.message}");
return Future.error("createMediaInformationSession failed.", stack);
}
}
/// Creates a new MediaInformation session from [sessionMap], which includes
/// session fields as map keys.
///
/// Returns MediaInformation session created.
static MediaInformationSession createMediaInformationSessionFromMap(
Map<dynamic, dynamic> sessionMap) {
final session = new MediaInformationSession();
session._sessionId = sessionMap["sessionId"];
session._createTime = FFmpegKitFactory.validDate(sessionMap["createTime"]);
session._startTime = FFmpegKitFactory.validDate(sessionMap["startTime"]);
session._command = sessionMap["command"];
session._argumentsArray =
FFmpegKitConfig.parseArguments(sessionMap["command"]);
session._logRedirectionStrategy = LogRedirectionStrategy.neverPrintLogs;
if (sessionMap.containsKey("mediaInformation")) {
session.setMediaInformation(
new MediaInformation(sessionMap["mediaInformation"]));
}
return session;
}
/// Returns the session specific log callback.
LogCallback? getLogCallback() =>
FFmpegKitFactory.getLogCallback(this.getSessionId());
/// Returns the session identifier.
int? getSessionId() => this._sessionId;
/// Returns session create time.
DateTime? getCreateTime() => this._createTime;
/// Returns session start time.
DateTime? getStartTime() => this._startTime;
/// Returns session end time.
Future<DateTime?> getEndTime() async {
try {
return _platform
.abstractSessionGetEndTime(this.getSessionId())
.then(FFmpegKitFactory.validDate);
} on PlatformException catch (e, stack) {
print("Plugin getEndTime error: ${e.message}");
return Future.error("getEndTime failed.", stack);
}
}
/// Returns time taken to execute this session in milliseconds or zero (0)
/// if the session is not over yet.
Future<int> getDuration() async {
try {
return _platform
.abstractSessionGetDuration(this.getSessionId())
.then((duration) => duration ?? 0);
} on PlatformException catch (e, stack) {
print("Plugin getDuration error: ${e.message}");
return Future.error("getDuration failed.", stack);
}
}
/// Returns command arguments as an array.
List<String>? getArguments() => this._argumentsArray;
/// Returns command arguments as a concatenated string.
String? getCommand() => this._command;
/// Returns all log entries generated for this session. If there are
/// asynchronous logs that are not delivered yet, this method waits for
/// them until [waitTimeout].
Future<List<Log>> getAllLogs([int? waitTimeout = null]) async {
try {
return _platform
.abstractSessionGetAllLogs(this.getSessionId(), waitTimeout)
.then((allLogs) {
if (allLogs == null) {
return List.empty();
} else {
return allLogs
.map((dynamic logObject) =>
FFmpegKitFactory.mapToLog(logObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getAllLogs error: ${e.message}");
return Future.error("getAllLogs failed.", stack);
}
}
/// Returns all log entries delivered for this session. Note that if there
/// are asynchronous logs that are not delivered yet, this method
/// will not wait for them and will return immediately.
Future<List<Log>> getLogs() async {
try {
return _platform
.abstractSessionGetLogs(this.getSessionId())
.then((allLogs) {
if (allLogs == null) {
return List.empty();
} else {
return allLogs
.map((dynamic logObject) =>
FFmpegKitFactory.mapToLog(logObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getLogs error: ${e.message}");
return Future.error("getLogs failed.", stack);
}
}
/// Returns all log entries generated for this session as a concatenated
/// string. If there are asynchronous logs that are not delivered yet,
/// this method waits for them until [waitTimeout].
Future<String?> getAllLogsAsString([int? waitTimeout = null]) async {
try {
return _platform.abstractSessionGetAllLogsAsString(
this.getSessionId(), waitTimeout);
} on PlatformException catch (e, stack) {
print("Plugin getAllLogsAsString error: ${e.message}");
return Future.error("getAllLogsAsString failed.", stack);
}
}
/// Returns all log entries delivered for this session as a concatenated
/// string. Note that if there are asynchronous logs that are not
/// delivered yet, this method will not wait for them and will return
/// immediately.
Future<String> getLogsAsString() async {
final StringBuffer concatenatedString = new StringBuffer();
void concatLog(Log log) => concatenatedString.write(log.getMessage());
final List<Log> logs = await this.getLogs();
logs.forEach(concatLog);
return concatenatedString.toString();
}
/// Returns the log output generated while running the session.
Future<String?> getOutput() async => this.getAllLogsAsString();
/// Returns the state of the session.
Future<SessionState> getState() async {
try {
return _platform
.abstractSessionGetState(this.getSessionId())
.then((state) {
switch (state) {
case 0:
return SessionState.created;
case 1:
return SessionState.running;
case 2:
return SessionState.failed;
case 3:
default:
return SessionState.completed;
}
});
} on PlatformException catch (e, stack) {
print("Plugin getState error: ${e.message}");
return Future.error("getState failed.", stack);
}
}
/// Returns the return code for this session. Note that return code is only
/// set for sessions that end with COMPLETED state. If a session is not
/// started, still running or failed then this method returns null.
Future<ReturnCode?> getReturnCode() async {
try {
return _platform
.abstractSessionGetReturnCode(this.getSessionId())
.then((returnCode) {
if (returnCode == null) {
return null;
} else {
return new ReturnCode(returnCode);
}
});
} on PlatformException catch (e, stack) {
print("Plugin getReturnCode error: ${e.message}");
return Future.error("getReturnCode failed.", stack);
}
}
/// Returns the stack trace of the exception received while executing this
/// session.
///
/// The stack trace is only set for sessions that end with FAILED state. For
/// sessions that has COMPLETED state this method returns null.
Future<String?> getFailStackTrace() async {
try {
return _platform.abstractSessionGetFailStackTrace(this.getSessionId());
} on PlatformException catch (e, stack) {
print("Plugin getFailStackTrace error: ${e.message}");
return Future.error("getFailStackTrace failed.", stack);
}
}
/// Returns session specific log redirection strategy.
LogRedirectionStrategy? getLogRedirectionStrategy() =>
this._logRedirectionStrategy;
/// Returns whether there are still asynchronous messages being transmitted
/// for this session or not.
Future<bool> thereAreAsynchronousMessagesInTransmit() async {
try {
return _platform.abstractSessionThereAreAsynchronousMessagesInTransmit(
this.getSessionId());
} on PlatformException catch (e, stack) {
print(
"Plugin thereAreAsynchronousMessagesInTransmit error: ${e.message}");
return Future.error(
"thereAreAsynchronousMessagesInTransmit failed.", stack);
}
}
/// Returns whether it is an "FFmpeg" session or not.
bool isFFmpeg() => false;
/// Returns whether it is an "FFprobe" session or not.
bool isFFprobe() => false;
/// Returns whether it is an "MediaInformation" session or not.
bool isMediaInformation() => false;
/// Cancels running the session.
Future<void> cancel() async {
try {
final int? sessionId = getSessionId();
await FFmpegKitConfig.init();
if (sessionId == null) {
return _platform.ffmpegKitCancel();
} else {
return _platform.ffmpegKitCancelSession(sessionId);
}
} on PlatformException catch (e, stack) {
print("Plugin cancel error: ${e.message}");
return Future.error("cancel failed.", stack);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
/// Detects the running architecture.
class ArchDetect {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Returns architecture name loaded.
static Future<String> getArch() async {
try {
await FFmpegKitConfig.init();
return _platform.archDetectGetArch();
} on PlatformException catch (e, stack) {
print("Plugin getArch error: ${e.message}");
return Future.error("getArch failed.", stack);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2021-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Chapter class.
class Chapter {
static const keyId = "id";
static const keyTimeBase = "time_base";
static const keyStart = "start";
static const keyStartTime = "start_time";
static const keyEnd = "end";
static const keyEndTime = "end_time";
static const keyTags = "tags";
Map<dynamic, dynamic>? _allProperties;
/// Creates a new [Chapter] instance
Chapter(this._allProperties);
/// Returns id.
int? getId() => this.getNumberProperty(Chapter.keyId)?.toInt();
/// Returns time base.
String? getTimeBase() => this.getStringProperty(Chapter.keyTimeBase);
/// Returns start.
int? getStart() => this.getNumberProperty(Chapter.keyStart)?.toInt();
/// Returns start time.
String? getStartTime() => this.getStringProperty(Chapter.keyStartTime);
/// Returns end.
int? getEnd() => this.getNumberProperty(Chapter.keyEnd)?.toInt();
/// Returns end time.
String? getEndTime() => this.getStringProperty(Chapter.keyEndTime);
/// Returns all tags.
Map<dynamic, dynamic>? getTags() => this.getProperty(Chapter.keyTags);
/// Returns the chapter property associated with the key.
String? getStringProperty(String key) => this._allProperties?[key];
/// Returns the chapter property associated with the key.
num? getNumberProperty(String key) => this._allProperties?[key];
/// Returns the chapter property associated with the key.
dynamic getProperty(String key) => this._allProperties?[key];
/// Returns all properties found.
Map<dynamic, dynamic>? getAllProperties() => this._allProperties;
}

View File

@ -0,0 +1,26 @@
import 'ffmpeg_session.dart';
import 'return_code.dart';
extension XFFmpegSession on Future<FFmpegSession> {
Future<String> thenReturnResultOrLogs(
String Function(FFmpegSession) onSuccess,
) =>
then(
(session) => session.getReturnCode().then(
(returnCode) {
if (ReturnCode.isSuccess(returnCode)) {
return onSuccess(session);
}
return session.getAllLogs().then(
(logs) {
final String logOutput =
logs.map((log) => log.getMessage()).join('\n');
throw Exception(
'FFmpeg command failed with return code: $returnCode. Logs: $logOutput',
);
},
);
},
),
);
}

View File

@ -0,0 +1,105 @@
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
import 'ffmpeg_session.dart';
import 'ffmpeg_session_complete_callback.dart';
import 'log_callback.dart';
import 'src/ffmpeg_kit_factory.dart';
import 'statistics_callback.dart';
/// Main class to run "FFmpeg" commands.
class FFmpegKit {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Synchronously executes FFmpeg command provided. Space character is used
/// to split command into arguments. You can use single or double quote
/// characters to specify arguments inside your command.
static Future<FFmpegSession> execute(String command) async =>
FFmpegKit.executeWithArguments(FFmpegKitConfig.parseArguments(command));
/// Synchronously executes FFmpeg with arguments provided.
static Future<FFmpegSession> executeWithArguments(
List<String> commandArguments) async {
final session =
await FFmpegSession.create(commandArguments, null, null, null, null);
await FFmpegKitConfig.ffmpegExecute(session);
return session;
}
/// Starts an asynchronous FFmpeg execution for the given command.
/// Space character is used to split the command
/// into arguments. You can use single or double quote characters
/// to specify arguments inside your command.
///
/// Note that this method returns immediately and does not wait
/// the execution to complete. You must use an
/// [FFmpegSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<FFmpegSession> executeAsync(String command,
[FFmpegSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
StatisticsCallback? statisticsCallback = null]) async =>
FFmpegKit.executeWithArgumentsAsync(
FFmpegKitConfig.parseArguments(command),
completeCallback,
logCallback,
statisticsCallback);
/// Starts an asynchronous FFmpeg execution with arguments provided.
///
/// Note that this method returns immediately and does not
/// wait the execution to complete. You must use an
/// [FFmpegSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<FFmpegSession> executeWithArgumentsAsync(
List<String> commandArguments,
[FFmpegSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
StatisticsCallback? statisticsCallback = null]) async {
final session = await FFmpegSession.create(commandArguments,
completeCallback, logCallback, statisticsCallback, null);
await FFmpegKitConfig.asyncFFmpegExecute(session);
return session;
}
/// Cancels the session specified with [sessionId].
static Future<void> cancel([int? sessionId = null]) async {
try {
await FFmpegKitConfig.init();
if (sessionId == null) {
return _platform.ffmpegKitCancel();
} else {
return _platform.ffmpegKitCancelSession(sessionId);
}
} on PlatformException catch (e, stack) {
print("Plugin cancel error: ${e.message}");
return Future.error("cancel failed.", stack);
}
}
/// Lists all FFmpeg sessions in the session history.
static Future<List<FFmpegSession>> listSessions() async {
try {
await FFmpegKitConfig.init();
return _platform.ffmpegKitListSessions().then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as FFmpegSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin listSessions error: ${e.message}");
return Future.error("listSessions failed.", stack);
}
}
}

View File

@ -0,0 +1,820 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_session.dart';
import 'ffmpeg_session_complete_callback.dart';
import 'ffprobe_session.dart';
import 'ffprobe_session_complete_callback.dart';
import 'level.dart';
import 'log_callback.dart';
import 'log_redirection_strategy.dart';
import 'media_information_session.dart';
import 'media_information_session_complete_callback.dart';
import 'session.dart';
import 'session_state.dart';
import 'signal.dart';
import 'src/ffmpeg_kit_factory.dart';
import 'src/ffmpeg_kit_flutter_initializer.dart';
import 'statistics_callback.dart';
/// Configuration class of "FFmpegKit" library.
class FFmpegKitConfig {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
static LogRedirectionStrategy _globalLogRedirectionStrategy =
LogRedirectionStrategy.printLogsWhenNoCallbacksDefined;
static int _activeLogLevel = Level.avLogTrace;
/// Initializes the library asynchronously.
static Future<void> init() async {
await FFmpegKitInitializer.initialize();
}
/// Enables log and statistics redirection.
///
/// When redirection is enabled FFmpeg/FFprobe logs are redirected to console
/// and sessions collect log and statistics entries for the executions. It
/// is possible to define global or session specific log/statistics callbacks
/// as well.
///
/// Note that redirection is enabled by default. If you do not want to use
/// its functionality please use [disableRedirection] to disable it.
static Future<void> enableRedirection() async {
try {
await init();
return _platform.ffmpegKitConfigEnableRedirection();
} on PlatformException catch (e, stack) {
print("Plugin enableRedirection error: ${e.message}");
return Future.error("enableRedirection failed.", stack);
}
}
/// Disables log and statistics redirection.
///
/// When redirection is disabled logs are printed to stderr, all logs and
/// statistics callbacks are disabled and "FFprobeKit.getMediaInformation"
/// methods do not work.
static Future<void> disableRedirection() async {
try {
await init();
return _platform.ffmpegKitConfigDisableRedirection();
} on PlatformException catch (e, stack) {
print("Plugin disableRedirection error: ${e.message}");
return Future.error("disableRedirection failed.", stack);
}
}
/// Sets and overrides "fontconfig" configuration directory.
static Future<void> setFontconfigConfigurationPath(String path) async {
try {
await init();
return _platform.ffmpegKitConfigSetFontconfigConfigurationPath(path);
} on PlatformException catch (e, stack) {
print("Plugin setFontconfigConfigurationPath error: ${e.message}");
return Future.error("setFontconfigConfigurationPath failed.", stack);
}
}
/// Registers the fonts inside the given path, so they become available to
/// use in FFmpeg filters.
///
/// Note that you need to use a package with "fontconfig" inside to be able
/// to use fonts in "FFmpeg".
static Future<void> setFontDirectory(String path,
[Map<String, String>? mapping = null]) async {
try {
await init();
return _platform.ffmpegKitConfigSetFontDirectory(path, mapping);
} on PlatformException catch (e, stack) {
print("Plugin setFontDirectory error: ${e.message}");
return Future.error("setFontDirectory failed.", stack);
}
}
/// Registers the fonts inside the given list of font directories, so they
/// become available to use in FFmpeg filters.
///
/// Note that you need to use a package with "fontconfig" inside to be able
/// to use fonts in "FFmpeg".
static Future<void> setFontDirectoryList(List<String> fontDirectoryList,
[Map<String, String>? mapping = null]) async {
try {
await init();
return _platform.ffmpegKitConfigSetFontDirectoryList(
fontDirectoryList, mapping);
} on PlatformException catch (e, stack) {
print("Plugin setFontDirectoryList error: ${e.message}");
return Future.error("setFontDirectoryList failed.", stack);
}
}
/// Creates a new named pipe to use in "FFmpeg" operations.
///
/// Please note that creator is responsible of closing created pipes.
static Future<String?> registerNewFFmpegPipe() async {
try {
await init();
return _platform.ffmpegKitConfigRegisterNewFFmpegPipe();
} on PlatformException catch (e, stack) {
print("Plugin registerNewFFmpegPipe error: ${e.message}");
return Future.error("registerNewFFmpegPipe failed.", stack);
}
}
/// Closes a previously created "FFmpeg" pipe.
static Future<void> closeFFmpegPipe(String ffmpegPipePath) async {
try {
await init();
return _platform.ffmpegKitConfigCloseFFmpegPipe(ffmpegPipePath);
} on PlatformException catch (e, stack) {
print("Plugin closeFFmpegPipe error: ${e.message}");
return Future.error("closeFFmpegPipe failed.", stack);
}
}
/// Returns the version of FFmpeg bundled within "FFmpegKit" library.
static Future<String?> getFFmpegVersion() async {
try {
await init();
return _platform.ffmpegKitConfigGetFFmpegVersion();
} on PlatformException catch (e, stack) {
print("Plugin getFFmpegVersion error: ${e.message}");
return Future.error("getFFmpegVersion failed.", stack);
}
}
/// Returns FFmpegKit Flutter library version.
static Future<String> getVersion() async => FFmpegKitFactory.getVersion();
/// Returns whether FFmpegKit release is a Long Term Release or not.
static Future<bool> isLTSBuild() async {
try {
await init();
return _platform.ffmpegKitConfigIsLTSBuild().then((value) {
if (value == null) {
return false;
} else {
return value;
}
});
} on PlatformException catch (e, stack) {
print("Plugin isLTSBuild error: ${e.message}");
return Future.error("isLTSBuild failed.", stack);
}
}
/// Returns FFmpegKit native library build date.
static Future<String?> getBuildDate() async {
try {
await init();
return _platform.ffmpegKitConfigGetBuildDate();
} on PlatformException catch (e, stack) {
print("Plugin getBuildDate error: ${e.message}");
return Future.error("getBuildDate failed.", stack);
}
}
/// Sets an environment variable.
static Future<void> setEnvironmentVariable(String name, String value) async {
try {
await init();
return _platform.ffmpegKitConfigSetEnvironmentVariable(name, value);
} on PlatformException catch (e, stack) {
print("Plugin setEnvironmentVariable error: ${e.message}");
return Future.error("setEnvironmentVariable failed.", stack);
}
}
/// Registers a new ignored signal. Ignored signals are not handled by
/// "FFmpegKit" library.
static Future<void> ignoreSignal(Signal signal) async {
try {
await init();
return _platform.ffmpegKitConfigIgnoreSignal(signal.index);
} on PlatformException catch (e, stack) {
print("Plugin ignoreSignal error: ${e.message}");
return Future.error("ignoreSignal failed.", stack);
}
}
/// Synchronously executes the FFmpeg session provided.
static Future<void> ffmpegExecute(FFmpegSession ffmpegSession) async {
try {
await init();
return _platform
.ffmpegKitConfigFFmpegExecute(ffmpegSession.getSessionId());
} on PlatformException catch (e, stack) {
print("Plugin ffmpegExecute error: ${e.message}");
return Future.error("ffmpegExecute failed.", stack);
}
}
/// Synchronously executes the FFprobe session provided.
static Future<void> ffprobeExecute(FFprobeSession ffprobeSession) async {
try {
await init();
return _platform
.ffmpegKitConfigFFprobeExecute(ffprobeSession.getSessionId());
} on PlatformException catch (e, stack) {
print("Plugin ffprobeExecute error: ${e.message}");
return Future.error("ffprobeExecute failed.", stack);
}
}
/// Synchronously executes the media information session provided.
static Future<void> getMediaInformationExecute(
MediaInformationSession mediaInformationSession,
[int? waitTimeout = null]) async {
try {
await init();
return _platform.ffmpegKitConfigGetMediaInformationExecute(
mediaInformationSession.getSessionId(), waitTimeout);
} on PlatformException catch (e, stack) {
print("Plugin getMediaInformationExecute error: ${e.message}");
return Future.error("getMediaInformationExecute failed.", stack);
}
}
/// Starts an asynchronous FFmpeg execution for the given session.
///
/// Note that this method returns immediately and does not wait the execution
/// to complete. You must use an
/// [FFmpegSessionCompleteCallback] if you want to be notified about
/// the result.
static Future<void> asyncFFmpegExecute(FFmpegSession ffmpegSession) async {
try {
await init();
return _platform
.ffmpegKitConfigAsyncFFmpegExecute(ffmpegSession.getSessionId());
} on PlatformException catch (e, stack) {
print("Plugin asyncFFmpegExecute error: ${e.message}");
return Future.error("asyncFFmpegExecute failed.", stack);
}
}
/// Starts an asynchronous FFprobe execution for the given session.
///
/// Note that this method returns immediately and does not wait the execution
/// to complete. You must use an
/// [FFprobeSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<void> asyncFFprobeExecute(FFprobeSession ffprobeSession) async {
try {
await init();
return _platform
.ffmpegKitConfigAsyncFFprobeExecute(ffprobeSession.getSessionId());
} on PlatformException catch (e, stack) {
print("Plugin asyncFFprobeExecute error: ${e.message}");
return Future.error("asyncFFprobeExecute failed.", stack);
}
}
/// Starts an asynchronous FFprobe execution for the given media information
/// session.
///
/// Note that this method returns immediately and does not wait the execution
/// to complete. You must use an
/// [MediaInformationSessionCompleteCallback] if you want to be notified about
/// the result.
static Future<void> asyncGetMediaInformationExecute(
MediaInformationSession mediaInformationSession,
[int? waitTimeout = null]) async {
try {
await init();
return _platform.ffmpegKitConfigAsyncGetMediaInformationExecute(
mediaInformationSession.getSessionId(), waitTimeout);
} on PlatformException catch (e, stack) {
print("Plugin asyncGetMediaInformationExecute error: ${e.message}");
return Future.error("asyncGetMediaInformationExecute failed.", stack);
}
}
/// Sets a global callback to redirect FFmpeg/FFprobe logs.
static void enableLogCallback([LogCallback? logCallback = null]) {
FFmpegKitFactory.setGlobalLogCallback(logCallback);
}
/// Sets a global callback to redirect FFmpeg statistics.
static void enableStatisticsCallback(
[StatisticsCallback? statisticsCallback = null]) {
FFmpegKitFactory.setGlobalStatisticsCallback(statisticsCallback);
}
/// Sets a global FFmpegSessionCompleteCallback to receive execution results
/// for FFmpeg sessions.
static void enableFFmpegSessionCompleteCallback(
[FFmpegSessionCompleteCallback? ffmpegSessionCompleteCallback = null]) {
FFmpegKitFactory.setGlobalFFmpegSessionCompleteCallback(
ffmpegSessionCompleteCallback);
}
/// Returns the global FFmpegSessionCompleteCallback set.
static FFmpegSessionCompleteCallback? getFFmpegSessionCompleteCallback() =>
FFmpegKitFactory.getGlobalFFmpegSessionCompleteCallback();
/// Sets a global FFprobeSessionCompleteCallback to receive execution results
/// for FFprobe sessions.
static void enableFFprobeSessionCompleteCallback(
[FFprobeSessionCompleteCallback? ffprobeSessionCompleteCallback = null]) {
FFmpegKitFactory.setGlobalFFprobeSessionCompleteCallback(
ffprobeSessionCompleteCallback);
}
/// Returns the global FFprobeSessionCompleteCallback set.
static FFprobeSessionCompleteCallback? getFFprobeSessionCompleteCallback() =>
FFmpegKitFactory.getGlobalFFprobeSessionCompleteCallback();
/// Sets a global MediaInformationSessionCompleteCallback to receive
/// execution results for MediaInformation sessions.
static void enableMediaInformationSessionCompleteCallback(
[MediaInformationSessionCompleteCallback?
mediaInformationSessionCompleteCallback = null]) {
FFmpegKitFactory.setGlobalMediaInformationSessionCompleteCallback(
mediaInformationSessionCompleteCallback);
}
/// Returns the global MediaInformationSessionCompleteCallback set.
static MediaInformationSessionCompleteCallback?
getMediaInformationSessionCompleteCallback() =>
FFmpegKitFactory.getGlobalMediaInformationSessionCompleteCallback();
/// Returns the current log level.
static int getLogLevel() => _activeLogLevel;
/// Sets the log level.
static Future<void> setLogLevel(int logLevel) async {
_activeLogLevel = logLevel;
try {
await init();
return _platform.ffmpegKitConfigSetLogLevel(logLevel);
} on PlatformException catch (e, stack) {
print("Plugin setLogLevel error: ${e.message}");
return Future.error("setLogLevel failed.", stack);
}
}
/// Converts the given Structured Access Framework Uri ("content:…") into
/// an input url that can be used in FFmpeg and FFprobe commands.
///
/// Note that this method is Android only. It will fail if called on other
/// platforms. It also requires API Level &ge; 19. On older API levels it
/// returns an empty url.
static Future<String?> getSafParameterForRead(String uriString) async {
try {
await init();
return _platform.ffmpegKitConfigGetSafParameter(uriString, "r");
} on PlatformException catch (e, stack) {
print("Plugin getSafParameterForRead error: ${e.message}");
return Future.error("getSafParameterForRead failed.", stack);
}
}
/// Converts the given Structured Access Framework Uri ("content:…") into
/// an output url that can be used in FFmpeg and FFprobe commands.
///
/// Note that this method is Android only. It will fail if called on other
/// platforms. It also requires API Level &ge; 19. On older API levels it
/// returns an empty url.
static Future<String?> getSafParameterForWrite(String uriString) async {
try {
await init();
return _platform.ffmpegKitConfigGetSafParameter(uriString, "w");
} on PlatformException catch (e, stack) {
print("Plugin getSafParameterForWrite error: ${e.message}");
return Future.error("getSafParameterForWrite failed.", stack);
}
}
/// Converts the given Structured Access Framework Uri into an saf protocol
/// url opened with the given open mode.
///
/// Note that this method is Android only. It will fail if called on other
/// platforms. It also requires API Level &ge; 19. On older API levels it
/// returns an empty url.
static Future<String?> getSafParameter(
String uriString, String openMode) async {
try {
await init();
return _platform.ffmpegKitConfigGetSafParameter(uriString, openMode);
} on PlatformException catch (e, stack) {
print("Plugin getSafParameter error: ${e.message}");
return Future.error("getSafParameter failed.", stack);
}
}
/// Returns the session history size.
static Future<int?> getSessionHistorySize() async {
try {
await init();
return _platform.ffmpegKitConfigGetSessionHistorySize();
} on PlatformException catch (e, stack) {
print("Plugin getSessionHistorySize error: ${e.message}");
return Future.error("getSessionHistorySize failed.", stack);
}
}
/// Sets the session history size.
static Future<void> setSessionHistorySize(int sessionHistorySize) async {
try {
await init();
return _platform.ffmpegKitConfigSetSessionHistorySize(sessionHistorySize);
} on PlatformException catch (e, stack) {
print("Plugin setSessionHistorySize error: ${e.message}");
return Future.error("setSessionHistorySize failed.", stack);
}
}
/// Returns the session specified with "sessionId" from the session history.
static Future<Session?> getSession(int sessionId) async {
try {
await init();
return _platform
.ffmpegKitConfigGetSession(sessionId)
.then(FFmpegKitFactory.mapToNullableSession);
} on PlatformException catch (e, stack) {
print("Plugin getSession error: ${e.message}");
return Future.error("getSession failed.", stack);
}
}
/// Returns the last session created from the session history.
static Future<Session?> getLastSession() async {
try {
await init();
return _platform
.ffmpegKitConfigGetLastSession()
.then(FFmpegKitFactory.mapToNullableSession);
} on PlatformException catch (e, stack) {
print("Plugin getLastSession error: ${e.message}");
return Future.error("getLastSession failed.", stack);
}
}
/// Returns the last session completed from the session history.
static Future<Session?> getLastCompletedSession() async {
try {
await init();
return _platform
.ffmpegKitConfigGetLastCompletedSession()
.then(FFmpegKitFactory.mapToNullableSession);
} on PlatformException catch (e, stack) {
print("Plugin getLastCompletedSession error: ${e.message}");
return Future.error("getLastCompletedSession failed.", stack);
}
}
/// Returns all sessions in the session history.
static Future<List<Session>> getSessions() async {
try {
await init();
return _platform.ffmpegKitConfigGetSessions().then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getSessions error: ${e.message}");
return Future.error("getSessions failed.", stack);
}
}
/// Clears all, including ongoing, sessions in the session history.
/// Note that callbacks cannot be triggered for deleted sessions.
static Future<void> clearSessions() async {
try {
await init();
return _platform.clearSessions();
} on PlatformException catch (e, stack) {
print("Plugin clearSessions error: ${e.message}");
return Future.error("clearSessions failed.", stack);
}
}
/// Returns all FFmpeg sessions in the session history.
static Future<List<FFmpegSession>> getFFmpegSessions() async {
try {
await FFmpegKitConfig.init();
return _platform.ffmpegKitListSessions().then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as FFmpegSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getFFmpegSessions error: ${e.message}");
return Future.error("getFFmpegSessions failed.", stack);
}
}
/// Returns all FFprobe sessions in the session history.
static Future<List<FFprobeSession>> getFFprobeSessions() async {
try {
await FFmpegKitConfig.init();
return _platform.ffprobeKitListFFprobeSessions().then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as FFprobeSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getFFprobeSessions error: ${e.message}");
return Future.error("getFFprobeSessions failed.", stack);
}
}
/// Returns all MediaInformation sessions in the session history.
static Future<List<MediaInformationSession>>
getMediaInformationSessions() async {
try {
await FFmpegKitConfig.init();
return _platform
.ffprobeKitListMediaInformationSessions()
.then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as MediaInformationSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getMediaInformationSessions error: ${e.message}");
return Future.error("getMediaInformationSessions failed.", stack);
}
}
/// Returns sessions that have [sessionState].
static Future<List<Session>> getSessionsByState(
SessionState sessionState) async {
try {
await init();
return _platform
.ffmpegKitConfigGetSessionsByState(sessionState.index)
.then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getSessionsByState error: ${e.message}");
return Future.error("getSessionsByState failed.", stack);
}
}
/// Returns the active log redirection strategy.
static LogRedirectionStrategy getLogRedirectionStrategy() =>
_globalLogRedirectionStrategy;
/// Sets the log redirection strategy.
static void setLogRedirectionStrategy(
LogRedirectionStrategy logRedirectionStrategy) {
_globalLogRedirectionStrategy = logRedirectionStrategy;
}
/// Returns the number of messages that are not transmitted to the
/// Flutter callbacks yet for this session.
static Future<int?> messagesInTransmit(int sessionId) async {
try {
await init();
return _platform.ffmpegKitConfigMessagesInTransmit(sessionId);
} on PlatformException catch (e, stack) {
print("Plugin messagesInTransmit error: ${e.message}");
return Future.error("messagesInTransmit failed.", stack);
}
}
/// Returns the string representation of the SessionState provided.
static String sessionStateToString(SessionState state) {
switch (state) {
case SessionState.created:
return "CREATED";
case SessionState.running:
return "RUNNING";
case SessionState.failed:
return "FAILED";
case SessionState.completed:
return "COMPLETED";
}
}
/// Parses [command] into arguments. Uses space character to split the
/// arguments. Supports single and double quote characters.
static List<String> parseArguments(String command) {
final List<String> argumentList = List<String>.empty(growable: true);
StringBuffer currentArgument = new StringBuffer();
bool singleQuoteStarted = false;
bool doubleQuoteStarted = false;
for (int i = 0; i < command.length; i++) {
int? previousChar;
if (i > 0) {
previousChar = command.codeUnitAt(i - 1);
} else {
previousChar = null;
}
final currentChar = command.codeUnitAt(i);
if (currentChar == ' '.codeUnitAt(0)) {
if (singleQuoteStarted || doubleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else if (currentArgument.length > 0) {
argumentList.add(currentArgument.toString());
currentArgument = new StringBuffer();
}
} else if (currentChar == '\''.codeUnitAt(0) &&
(previousChar == null || previousChar != '\\'.codeUnitAt(0))) {
if (singleQuoteStarted) {
singleQuoteStarted = false;
} else if (doubleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else {
singleQuoteStarted = true;
}
} else if (currentChar == '\"'.codeUnitAt(0) &&
(previousChar == null || previousChar != '\\'.codeUnitAt(0))) {
if (doubleQuoteStarted) {
doubleQuoteStarted = false;
} else if (singleQuoteStarted) {
currentArgument.write(String.fromCharCode(currentChar));
} else {
doubleQuoteStarted = true;
}
} else {
currentArgument.write(String.fromCharCode(currentChar));
}
}
if (currentArgument.length > 0) {
argumentList.add(currentArgument.toString());
}
return argumentList;
}
/// Concatenates arguments into a string adding a space character between
/// two arguments.
static String argumentsToString(List<String>? arguments) {
if (arguments == null) {
return "null";
}
final StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < arguments.length; i++) {
if (i > 0) {
stringBuffer.write(" ");
}
stringBuffer.write(arguments[i]);
}
return stringBuffer.toString();
}
// THE FOLLOWING METHODS ARE FLUTTER SPECIFIC
/// Enables logs.
static Future<void> enableLogs() async {
try {
await init();
return _platform.ffmpegKitConfigEnableLogs();
} on PlatformException catch (e, stack) {
print("Plugin enableLogs error: ${e.message}");
return Future.error("enableLogs failed.", stack);
}
}
/// Disable logs.
static Future<void> disableLogs() async {
try {
await init();
return _platform.ffmpegKitConfigDisableLogs();
} on PlatformException catch (e, stack) {
print("Plugin disableLogs error: ${e.message}");
return Future.error("disableLogs failed.", stack);
}
}
/// Enables statistics.
static Future<void> enableStatistics() async {
try {
await init();
return _platform.ffmpegKitConfigEnableStatistics();
} on PlatformException catch (e, stack) {
print("Plugin enableStatistics error: ${e.message}");
return Future.error("enableStatistics failed.", stack);
}
}
/// Disables statistics.
static Future<void> disableStatistics() async {
try {
await init();
return _platform.ffmpegKitConfigDisableStatistics();
} on PlatformException catch (e, stack) {
print("Plugin disableStatistics error: ${e.message}");
return Future.error("disableStatistics failed.", stack);
}
}
/// Returns the platform name the library is loaded on.
static Future<String?> getPlatform() async {
try {
await init();
return _platform.ffmpegKitConfigGetPlatform();
} on PlatformException catch (e, stack) {
print("Plugin getPlatform error: ${e.message}");
return Future.error("getPlatform failed.", stack);
}
}
/// Writes [inputPath] to [pipePath].
static Future<int?> writeToPipe(String inputPath, String pipePath) async {
try {
await init();
return _platform.ffmpegKitConfigWriteToPipe(inputPath, pipePath);
} on PlatformException catch (e, stack) {
print("Plugin writeToPipe error: ${e.message}");
return Future.error("writeToPipe failed.", stack);
}
}
/// Displays the native file dialog to select a file in read mode. If a file
/// is selected then this method returns the Structured Access Framework Uri
/// ("content:…") for that file.
///
/// Note that this method is Android only. It will fail if called on other
/// platforms.
static Future<String?> selectDocumentForRead(
[String? type = null, List<String>? extraTypes = null]) async {
try {
await init();
return _platform.ffmpegKitConfigSelectDocumentForRead(type, extraTypes);
} on PlatformException catch (e, stack) {
print("Plugin selectDocumentForRead error: ${e.message}");
return Future.error("selectDocumentForRead failed.", stack);
}
}
/// Displays the native file dialog to select a file in write mode. If a file
/// is selected then this method returns the Structured Access Framework Uri
/// ("content:…") for that file.
///
/// Note that this method is Android only. It will fail if called on other
/// platforms.
static Future<String?> selectDocumentForWrite(
[String? title = null,
String? type = null,
List<String>? extraTypes = null]) async {
try {
await init();
return _platform.ffmpegKitConfigSelectDocumentForWrite(
title, type, extraTypes);
} on PlatformException catch (e, stack) {
print("Plugin selectDocumentForWrite error: ${e.message}");
return Future.error("selectDocumentForWrite failed.", stack);
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'abstract_session.dart';
import 'ffmpeg_kit_config.dart';
import 'ffmpeg_session_complete_callback.dart';
import 'log_callback.dart';
import 'log_redirection_strategy.dart';
import 'src/ffmpeg_kit_factory.dart';
import 'statistics.dart';
import 'statistics_callback.dart';
/// An FFmpeg session.
class FFmpegSession extends AbstractSession {
/// Creates a new FFmpeg session with [argumentsArray].
static Future<FFmpegSession> create(List<String> argumentsArray,
[FFmpegSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
StatisticsCallback? statisticsCallback = null,
LogRedirectionStrategy? logRedirectionStrategy = null]) async {
final session = await AbstractSession.createFFmpegSession(
argumentsArray, logRedirectionStrategy);
final sessionId = session.getSessionId();
FFmpegKitFactory.setFFmpegSessionCompleteCallback(
sessionId, completeCallback);
FFmpegKitFactory.setLogCallback(sessionId, logCallback);
FFmpegKitFactory.setStatisticsCallback(sessionId, statisticsCallback);
return session;
}
/// Returns the session specific statistics callback.
StatisticsCallback? getStatisticsCallback() =>
FFmpegKitFactory.getStatisticsCallback(this.getSessionId());
/// Returns the session specific complete callback.
FFmpegSessionCompleteCallback? getCompleteCallback() =>
FFmpegKitFactory.getFFmpegSessionCompleteCallback(this.getSessionId());
/// Returns all statistics entries generated for this session. If there are
/// asynchronous statistics that are not delivered yet, this method waits for
/// them until [waitTimeout].
Future<List<Statistics>> getAllStatistics([int? waitTimeout]) async {
try {
await FFmpegKitConfig.init();
return FFmpegKitPlatform.instance
.ffmpegSessionGetAllStatistics(this.getSessionId(), waitTimeout)
.then((allStatistics) {
if (allStatistics == null) {
return List.empty();
} else {
return allStatistics
.map((dynamic statisticsObject) =>
FFmpegKitFactory.mapToStatistics(
statisticsObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getAllStatistics error: ${e.message}");
return Future.error("getAllStatistics failed.", stack);
}
}
/// Returns all statistics entries delivered for this session. Note that if
/// there are asynchronous statistics that are not delivered yet, this method
/// will not wait for them and will return immediately.
Future<List<Statistics>> getStatistics() async {
try {
await FFmpegKitConfig.init();
return FFmpegKitPlatform.instance
.ffmpegSessionGetStatistics(this.getSessionId())
.then((statistics) {
if (statistics == null) {
return List.empty();
} else {
return statistics
.map((dynamic statisticsObject) =>
FFmpegKitFactory.mapToStatistics(
statisticsObject as Map<dynamic, dynamic>))
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getStatistics error: ${e.message}");
return Future.error("getStatistics failed.", stack);
}
}
/// Returns the last received statistics entry.
Future<Statistics?> getLastReceivedStatistics() async =>
this.getStatistics().then((statistics) {
if (statistics.isNotEmpty) {
return statistics[statistics.length - 1];
} else {
return null;
}
});
bool isFFmpeg() => true;
bool isFFprobe() => false;
bool isMediaInformation() => false;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'ffmpeg_session.dart';
/// Callback function that is invoked when an asynchronous FFmpeg session has
/// ended. Session has either SessionState.completed or SessionState.failed
/// state when the callback is invoked.
/// If it has SessionState.completed state, "ReturnCode" should be checked to
/// see the execution result.
/// If "getState" returns SessionState.failed then "getFailStackTrace" should
/// be used to get the failure reason.
typedef FFmpegSessionCompleteCallback = void Function(FFmpegSession session);

View File

@ -0,0 +1,257 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
import 'ffprobe_session.dart';
import 'ffprobe_session_complete_callback.dart';
import 'log_callback.dart';
import 'media_information_session.dart';
import 'media_information_session_complete_callback.dart';
import 'src/ffmpeg_kit_factory.dart';
/// Main class to run "FFprobe" commands.
class FFprobeKit {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Synchronously executes FFprobe command provided. Space character is used
/// to split command into arguments. You can use single or double quote
/// characters to specify arguments inside your command.
static Future<FFprobeSession> execute(String command) async =>
FFprobeKit.executeWithArguments(FFmpegKitConfig.parseArguments(command));
/// Synchronously executes FFprobe with arguments provided.
static Future<FFprobeSession> executeWithArguments(
List<String> commandArguments) async {
final session =
await FFprobeSession.create(commandArguments, null, null, null);
await FFmpegKitConfig.ffprobeExecute(session);
return session;
}
/// Starts an asynchronous FFprobe execution for the given command.
/// Space character is used to split the command
/// into arguments. You can use single or double quote characters
/// to specify arguments inside your command.
///
/// Note that this method returns immediately and does not wait the
/// execution to complete. You must use an
/// [FFprobeSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<FFprobeSession> executeAsync(String command,
[FFprobeSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null]) async =>
FFprobeKit.executeWithArgumentsAsync(
FFmpegKitConfig.parseArguments(command),
completeCallback,
logCallback);
/// Starts an asynchronous FFprobe execution with arguments provided.
///
/// Note that this method returns immediately and does not wait the
/// execution to complete. You must use an
/// [FFprobeSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<FFprobeSession> executeWithArgumentsAsync(
List<String> commandArguments,
[FFprobeSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null]) async {
final session = await FFprobeSession.create(
commandArguments, completeCallback, logCallback, null);
await FFmpegKitConfig.asyncFFprobeExecute(session);
return session;
}
/// Extracts media information for the file specified with path.
static Future<MediaInformationSession> getMediaInformation(String path,
[int? waitTimeout = null]) async {
final commandArguments = [
"-v",
"error",
"-hide_banner",
"-print_format",
"json",
"-show_format",
"-show_streams",
"-show_chapters",
"-i",
path
];
return FFprobeKit.getMediaInformationFromCommandArguments(
commandArguments, waitTimeout);
}
/// Extracts media information using the command provided. The command
/// passed to this method must generate the output in JSON format in order
/// to successfully extract media information from it.
static Future<MediaInformationSession> getMediaInformationFromCommand(
String command,
[int? waitTimeout = null]) async =>
FFprobeKit.getMediaInformationFromCommandArguments(
FFmpegKitConfig.parseArguments(command), waitTimeout);
/// Extracts media information using the command arguments provided. The
/// command passed to this method must generate the output in JSON format in
/// order to successfully extract media information from it.
static Future<MediaInformationSession>
getMediaInformationFromCommandArguments(List<String> commandArguments,
[int? waitTimeout = null]) async {
final session =
await MediaInformationSession.create(commandArguments, null, null);
await FFmpegKitConfig.getMediaInformationExecute(session, waitTimeout);
final mediaInformation = await _platform
.mediaInformationSessionGetMediaInformation(session.getSessionId())
.then(FFmpegKitFactory.mapToNullableMediaInformation);
if (mediaInformation != null) {
session.setMediaInformation(mediaInformation);
}
return session;
}
/// Starts an asynchronous FFprobe execution to extract the media information
/// for the specified file.
///
/// Note that this method returns immediately and does not wait the execution
/// to complete. You must use an
/// [MediaInformationSessionCompleteCallback] if you want to be notified about
/// the result.
static Future<MediaInformationSession> getMediaInformationAsync(String path,
[MediaInformationSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
int? waitTimeout = null]) async {
final commandArguments = [
"-v",
"error",
"-hide_banner",
"-print_format",
"json",
"-show_format",
"-show_streams",
"-show_chapters",
"-i",
path
];
return FFprobeKit.getMediaInformationFromCommandArgumentsAsync(
commandArguments, completeCallback, logCallback, waitTimeout);
}
/// Starts an asynchronous FFprobe execution to extract media information
/// using a command. The command passed to
/// this method must generate the output in JSON format in order
/// to successfully extract media information from it.
///
/// Note that this method returns immediately and does not wait the
/// execution to complete. You must use an
/// [MediaInformationSessionCompleteCallback] if you want to be notified
/// about the result.
static Future<MediaInformationSession> getMediaInformationFromCommandAsync(
String command,
[MediaInformationSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
int? waitTimeout = null]) async =>
FFprobeKit.getMediaInformationFromCommandArgumentsAsync(
FFmpegKitConfig.parseArguments(command),
completeCallback,
logCallback,
waitTimeout);
/// Starts an asynchronous FFprobe execution to extract media information
/// using command arguments. The command passed to this method must generate
/// the output in JSON format in order to successfully extract media
/// information from it.
///
/// Note that this method returns immediately and does not wait the execution
/// to complete. You must use an [MediaInformationSessionCompleteCallback]
/// if you want to be
/// notified about the result.
static Future<MediaInformationSession>
getMediaInformationFromCommandArgumentsAsync(
List<String> commandArguments,
[MediaInformationSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
int? waitTimeout = null]) async {
final session = await MediaInformationSession.create(
commandArguments, completeCallback, logCallback);
await FFmpegKitConfig.asyncGetMediaInformationExecute(session, waitTimeout);
final mediaInformation = await _platform
.mediaInformationSessionGetMediaInformation(session.getSessionId())
.then(FFmpegKitFactory.mapToNullableMediaInformation);
if (mediaInformation != null) {
session.setMediaInformation(mediaInformation);
}
return session;
}
/// Lists all FFprobe sessions in the session history.
static Future<List<FFprobeSession>> listFFprobeSessions() async {
try {
await FFmpegKitConfig.init();
return _platform.ffprobeKitListFFprobeSessions().then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as FFprobeSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin listFFprobeSessions error: ${e.message}");
return Future.error("listFFprobeSessions failed.", stack);
}
}
/// Lists all MediaInformation sessions in the session history.
static Future<List<MediaInformationSession>>
listMediaInformationSessions() async {
try {
await FFmpegKitConfig.init();
return _platform
.ffprobeKitListMediaInformationSessions()
.then((sessions) {
if (sessions == null) {
return List.empty();
} else {
return sessions
.map((dynamic sessionObject) => FFmpegKitFactory.mapToSession(
sessionObject as Map<dynamic, dynamic>))
.map((session) => session as MediaInformationSession)
.toList();
}
});
} on PlatformException catch (e, stack) {
print("Plugin listMediaInformationSessions error: ${e.message}");
return Future.error("listMediaInformationSessions failed.", stack);
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'abstract_session.dart';
import 'ffprobe_session_complete_callback.dart';
import 'log_callback.dart';
import 'log_redirection_strategy.dart';
import 'src/ffmpeg_kit_factory.dart';
/// An FFprobe session.
class FFprobeSession extends AbstractSession {
/// Creates a new FFprobe session with [argumentsArray].
static Future<FFprobeSession> create(List<String> argumentsArray,
[FFprobeSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null,
LogRedirectionStrategy? logRedirectionStrategy = null]) async {
final session = await AbstractSession.createFFprobeSession(
argumentsArray, logRedirectionStrategy);
final sessionId = session.getSessionId();
FFmpegKitFactory.setFFprobeSessionCompleteCallback(
sessionId, completeCallback);
FFmpegKitFactory.setLogCallback(sessionId, logCallback);
return session;
}
/// Returns the session specific complete callback.
FFprobeSessionCompleteCallback? getCompleteCallback() =>
FFmpegKitFactory.getFFprobeSessionCompleteCallback(this.getSessionId());
bool isFFmpeg() => false;
bool isFFprobe() => true;
bool isMediaInformation() => false;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'ffprobe_session.dart';
/// Callback function that is invoked when an asynchronous FFprobe session has
/// ended. Session has either SessionState.completed or SessionState.failed
/// state when the callback is invoked.
/// If it has SessionState.completed state, "ReturnCode" should be checked to
/// see the execution result.
/// If "getState" returns SessionState.failed then "getFailStackTrace" should
/// be used to get the failure reason.
typedef FFprobeSessionCompleteCallback = void Function(FFprobeSession session);

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Defines log levels.
class Level {
/// This log level is defined by FFmpegKit. It is used to specify logs
/// printed to stderr by FFmpeg. Logs that has this level are not filtered
/// and always redirected.
static const int avLogStderr = -16;
/// Print no output.
static const int avLogQuiet = -8;
/// Something went really wrong and we will crash now.
static const int avLogPanic = 0;
/// Something went wrong and recovery is not possible.
/// For example, no header was found for a format which depends
/// on headers or an illegal combination of parameters is used.
static const int avLogFatal = 8;
/// Something went wrong and cannot losslessly be recovered.
/// However, not all future data is affected.
static const int avLogError = 16;
/// Something somehow does not look correct. This may or may not
/// lead to problems. An example would be the use of '-vstrict -2'.
static const int avLogWarning = 24;
/// int Standard information.
static const int avLogInfo = 32;
/// Detailed information.
static const int avLogVerbose = 40;
/// Stuff which is only useful for libav* developers.
static const int avLogDebug = 48;
/// Extremely verbose debugging, useful for libav* development.
static const int avLogTrace = 56;
/// Returns log level string from int
static String levelToString(int level) {
switch (level) {
case Level.avLogTrace:
return "TRACE";
case Level.avLogDebug:
return "DEBUG";
case Level.avLogVerbose:
return "VERBOSE";
case Level.avLogInfo:
return "INFO";
case Level.avLogWarning:
return "WARNING";
case Level.avLogError:
return "ERROR";
case Level.avLogFatal:
return "FATAL";
case Level.avLogPanic:
return "PANIC";
case Level.avLogStderr:
return "STDERR";
case Level.avLogQuiet:
default:
return "";
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Log entry for an FFmpegKit session.
class Log {
int _sessionId;
int _level;
String _message;
Log(this._sessionId, this._level, this._message);
int getSessionId() => _sessionId;
int getLevel() => _level;
String getMessage() => _message;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'log.dart';
/// Callback function that receives logs generated for "FFmpegKit" sessions.
typedef LogCallback = void Function(Log log);

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
enum LogRedirectionStrategy {
alwaysPrintLogs,
printLogsWhenNoCallbacksDefined,
printLogsWhenGlobalCallbackNotDefined,
printLogsWhenSessionCallbackNotDefined,
neverPrintLogs
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'chapter.dart';
import 'stream_information.dart';
/// Media information class.
class MediaInformation {
static const keyFormatProperties = "format";
static const keyFilename = "filename";
static const keyFormat = "format_name";
static const keyFormatLong = "format_long_name";
static const keyStartTime = "start_time";
static const keyDuration = "duration";
static const keySize = "size";
static const keyBitRate = "bit_rate";
static const keyTags = "tags";
Map<dynamic, dynamic>? _allProperties;
/// Creates a new [MediaInformation] instance
MediaInformation(this._allProperties);
/// Returns file name.
String? getFilename() =>
this.getStringFormatProperty(MediaInformation.keyFilename);
/// Returns format.
String? getFormat() =>
this.getStringFormatProperty(MediaInformation.keyFormat);
/// Returns long format.
String? getLongFormat() =>
this.getStringFormatProperty(MediaInformation.keyFormatLong);
/// Returns duration.
String? getDuration() =>
this.getStringFormatProperty(MediaInformation.keyDuration);
/// Returns start time.
String? getStartTime() =>
this.getStringFormatProperty(MediaInformation.keyStartTime);
/// Returns size.
String? getSize() => this.getStringFormatProperty(MediaInformation.keySize);
/// Returns bitrate.
String? getBitrate() =>
this.getStringFormatProperty(MediaInformation.keyBitRate);
/// Returns all tags.
Map<dynamic, dynamic>? getTags() =>
this.getFormatProperty(StreamInformation.keyTags);
/// Returns the property associated with the key.
String? getStringProperty(String key) => this.getAllProperties()?[key];
/// Returns the property associated with the key.
num? getNumberProperty(String key) => this.getAllProperties()?[key];
/// Returns the property associated with the key.
dynamic getProperty(String key) => this.getAllProperties()?[key];
/// Returns the format property associated with the key.
String? getStringFormatProperty(String key) =>
this.getFormatProperties()?[key];
/// Returns the format property associated with the key.
num? getNumberFormatProperty(String key) => this.getFormatProperties()?[key];
/// Returns the format property associated with the key.
dynamic getFormatProperty(String key) => this.getFormatProperties()?[key];
/// Returns all streams found as a list.
List<StreamInformation> getStreams() {
final List<StreamInformation> list =
List<StreamInformation>.empty(growable: true);
dynamic createStreamInformation(Map<dynamic, dynamic> streamProperties) =>
list.add(new StreamInformation(streamProperties));
this._allProperties?["streams"]?.forEach((Object? stream) {
createStreamInformation(stream as Map<dynamic, dynamic>);
});
return list;
}
/// Returns all chapters found as a list.
List<Chapter> getChapters() {
final List<Chapter> list = List<Chapter>.empty(growable: true);
dynamic createChapter(Map<dynamic, dynamic> chapterProperties) =>
list.add(new Chapter(chapterProperties));
this._allProperties?["chapters"]?.forEach((Object? chapter) {
createChapter(chapter as Map<dynamic, dynamic>);
});
return list;
}
/// Returns all format properties found.
Map<dynamic, dynamic>? getFormatProperties() =>
this._allProperties?[keyFormatProperties];
/// Returns all properties found, including stream properties.
Map<dynamic, dynamic>? getAllProperties() => this._allProperties;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
import 'media_information.dart';
/// A parser that constructs "MediaInformation" from FFprobe's json output.
class MediaInformationJsonParser {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Extracts MediaInformation from the given FFprobe json output. Note that
/// this method does not fail as [fromWithError] does and returns null on
/// error.
static Future<MediaInformation?> from(String ffprobeJsonOutput) async {
try {
await FFmpegKitConfig.init();
return _platform
.mediaInformationJsonParserFrom(ffprobeJsonOutput)
.then((properties) {
if (properties == null || properties.isEmpty) {
return null;
} else {
return new MediaInformation(properties);
}
});
} on PlatformException catch (e, stack) {
print("Plugin from error: ${e.message}");
return Future.error("from failed.", stack);
}
}
/// Extracts MediaInformation from the given FFprobe json output.
static Future<MediaInformation> fromWithError(
String ffprobeJsonOutput) async {
try {
await FFmpegKitConfig.init();
return _platform
.mediaInformationJsonParserFromWithError(ffprobeJsonOutput)
.then(MediaInformation.new);
} on PlatformException catch (e, stack) {
print("Plugin fromWithError error: ${e.message}");
return Future.error("fromWithError failed.", stack);
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'abstract_session.dart';
import 'log_callback.dart';
import 'media_information.dart';
import 'media_information_session_complete_callback.dart';
import 'src/ffmpeg_kit_factory.dart';
/// A custom FFprobe session, which produces a "MediaInformation" object
/// using the FFprobe output.
class MediaInformationSession extends AbstractSession {
MediaInformation? _mediaInformation;
/// Creates a new MediaInformation session with [argumentsArray].
static Future<MediaInformationSession> create(List<String> argumentsArray,
[MediaInformationSessionCompleteCallback? completeCallback = null,
LogCallback? logCallback = null]) async {
final session =
await AbstractSession.createMediaInformationSession(argumentsArray);
final sessionId = session.getSessionId();
FFmpegKitFactory.setMediaInformationSessionCompleteCallback(
sessionId, completeCallback);
FFmpegKitFactory.setLogCallback(sessionId, logCallback);
return session;
}
/// Returns the media information extracted in this session.
MediaInformation? getMediaInformation() => this._mediaInformation;
/// Sets the media information extracted in this session.
void setMediaInformation(MediaInformation? mediaInformation) {
this._mediaInformation = mediaInformation;
}
/// Returns the session specific complete callback.
MediaInformationSessionCompleteCallback? getCompleteCallback() =>
FFmpegKitFactory.getMediaInformationSessionCompleteCallback(
this.getSessionId());
bool isFFmpeg() => false;
bool isFFprobe() => false;
bool isMediaInformation() => true;
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'media_information_session.dart';
/// Callback function that is invoked when an asynchronous MediaInformation
/// session has ended. Session has either SessionState.completed or
/// SessionState.failed state when the callback is invoked.
/// If it has SessionState.completed state, "ReturnCode" should be checked to
/// see the execution result.
/// If "getState" returns SessionState.failed then "getFailStackTrace" should
/// be used to get the failure reason.
typedef MediaInformationSessionCompleteCallback = void Function(
MediaInformationSession session);

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/services.dart';
import 'ffmpeg_kit_config.dart';
/// Helper class to extract binary package information.
class Packages {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
/// Returns the FFmpegKit Flutter binary package name.
static Future<String?> getPackageName() async {
try {
await FFmpegKitConfig.init();
return _platform.getPackageName();
} on PlatformException catch (e, stack) {
print("Plugin getPackageName error: ${e.message}");
return Future.error("getPackageName failed.", stack);
}
}
/// Returns enabled external libraries by FFmpeg.
static Future<List<String>> getExternalLibraries() async {
try {
await FFmpegKitConfig.init();
return _platform.getExternalLibraries().then((externalLibraries) {
if (externalLibraries == null) {
return List.empty();
} else {
return externalLibraries.cast<String>();
}
});
} on PlatformException catch (e, stack) {
print("Plugin getExternalLibraries error: ${e.message}");
return Future.error("getExternalLibraries failed.", stack);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
class ReturnCode {
static const int success = 0;
static const int cancel = 255;
int _value;
ReturnCode(this._value);
static bool isSuccess(ReturnCode? returnCode) =>
returnCode?.getValue() == ReturnCode.success;
static bool isCancel(ReturnCode? returnCode) =>
returnCode?.getValue() == ReturnCode.cancel;
int getValue() => this._value;
bool isValueSuccess() => this._value == ReturnCode.success;
bool isValueError() =>
(this._value != ReturnCode.success) && (this._value != ReturnCode.cancel);
bool isValueCancel() => this._value == ReturnCode.cancel;
String toString() => this._value.toString();
}

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'log.dart';
import 'log_callback.dart';
import 'log_redirection_strategy.dart';
import 'return_code.dart';
import 'session_state.dart';
/// Common interface for all "FFmpegKit" sessions.
abstract class Session {
/// Returns the session specific log callback.
LogCallback? getLogCallback();
/// Returns the session identifier.
int? getSessionId();
/// Returns session create time.
DateTime? getCreateTime();
/// Returns session start time.
DateTime? getStartTime();
/// Returns session end time.
Future<DateTime?> getEndTime();
/// Returns time taken to execute this session in milliseconds or zero (0)
/// if the session is not over yet.
Future<int> getDuration();
/// Returns command arguments as an array.
List<String>? getArguments();
/// Returns command arguments as a concatenated string.
String? getCommand();
/// Returns all log entries generated for this session. If there are
/// asynchronous logs that are not delivered yet, this method waits for
/// them until [waitTimeout].
Future<List<Log>> getAllLogs([int? waitTimeout = null]);
/// Returns all log entries delivered for this session. Note that if there
/// are asynchronous logs that are not delivered yet, this method
/// will not wait for them and will return immediately.
Future<List<Log>> getLogs();
/// Returns all log entries generated for this session as a concatenated
/// string. If there are asynchronous logs that are not delivered yet,
/// this method waits for them until [waitTimeout].
Future<String?> getAllLogsAsString([int? waitTimeout = null]);
/// Returns all log entries delivered for this session as a concatenated
/// string. Note that if there are asynchronous logs that are not
/// delivered yet, this method will not wait for them and will return
/// immediately.
Future<String> getLogsAsString();
/// Returns the log output generated while running the session.
Future<String?> getOutput();
/// Returns the state of the session.
Future<SessionState> getState();
/// Returns the return code for this session. Note that return code is only
/// set for sessions that end with COMPLETED state. If a session is not
/// started, still running or failed then this method returns null.
Future<ReturnCode?> getReturnCode();
/// Returns the stack trace of the exception received while executing this
/// session.
///
/// The stack trace is only set for sessions that end with FAILED state. For
/// sessions that has COMPLETED state this method returns null.
Future<String?> getFailStackTrace();
/// Returns session specific log redirection strategy.
LogRedirectionStrategy? getLogRedirectionStrategy();
/// Returns whether there are still asynchronous messages being transmitted
/// for this session or not.
Future<bool> thereAreAsynchronousMessagesInTransmit();
/// Returns whether it is an "FFmpeg" session or not.
bool isFFmpeg();
/// Returns whether it is an "FFprobe" session or not.
bool isFFprobe();
/// Returns whether it is an "MediaInformation" session or not.
bool isMediaInformation();
/// Cancels running the session.
Future<void> cancel();
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
enum SessionState { created, running, failed, completed }

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Signals handled by FFmpegKit library.
enum Signal { sigInt, sigQuit, sigPipe, sigTerm, sigXCpu }

View File

@ -0,0 +1,214 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import '../abstract_session.dart';
import '../ffmpeg_session_complete_callback.dart';
import '../ffprobe_session_complete_callback.dart';
import '../log.dart';
import '../log_callback.dart';
import '../log_redirection_strategy.dart';
import '../media_information.dart';
import '../media_information_session_complete_callback.dart';
import '../session.dart';
import '../statistics.dart';
import '../statistics_callback.dart';
final ffmpegSessionCompleteCallbackMap =
new Map<int, FFmpegSessionCompleteCallback>();
final ffprobeSessionCompleteCallbackMap =
new Map<int, FFprobeSessionCompleteCallback>();
final mediaInformationSessionCompleteCallbackMap =
new Map<int, MediaInformationSessionCompleteCallback>();
final logCallbackMap = new Map<int, LogCallback>();
final statisticsCallbackMap = new Map<int, StatisticsCallback>();
final logRedirectionStrategyMap = new Map<int, LogRedirectionStrategy>();
class FFmpegKitFactory {
static LogCallback? _logCallback;
static StatisticsCallback? _statisticsCallback;
static FFmpegSessionCompleteCallback? _ffmpegSessionCompleteCallback;
static FFprobeSessionCompleteCallback? _ffprobeSessionCompleteCallback;
static MediaInformationSessionCompleteCallback?
_mediaInformationSessionCompleteCallback;
static Statistics mapToStatistics(Map<dynamic, dynamic> statisticsMap) =>
new Statistics(
statisticsMap["sessionId"].toInt(),
statisticsMap["videoFrameNumber"].toInt(),
statisticsMap["videoFps"],
statisticsMap["videoQuality"],
statisticsMap["size"].toInt(),
statisticsMap["time"].toInt(),
statisticsMap["bitrate"],
statisticsMap["speed"]);
static Log mapToLog(Map<dynamic, dynamic> logMap) =>
new Log(logMap["sessionId"], logMap["level"], logMap["message"]);
static Session mapToSession(Map<dynamic, dynamic> sessionMap) {
switch (sessionMap["type"]) {
case 2:
return AbstractSession.createFFprobeSessionFromMap(sessionMap);
case 3:
return AbstractSession.createMediaInformationSessionFromMap(sessionMap);
case 1:
default:
return AbstractSession.createFFmpegSessionFromMap(sessionMap);
}
}
static Session? mapToNullableSession(Map<dynamic, dynamic>? sessionMap) {
if (sessionMap != null) {
switch (sessionMap["type"]) {
case 2:
return AbstractSession.createFFprobeSessionFromMap(sessionMap);
case 3:
return AbstractSession.createMediaInformationSessionFromMap(
sessionMap);
case 1:
default:
return AbstractSession.createFFmpegSessionFromMap(sessionMap);
}
} else {
return null;
}
}
static MediaInformation? mapToNullableMediaInformation(
Map<dynamic, dynamic>? mediaInformationMap) {
if (mediaInformationMap != null) {
return new MediaInformation(mediaInformationMap);
} else {
return null;
}
}
static String getVersion() => "8.0.0";
static LogRedirectionStrategy? getLogRedirectionStrategy(int? sessionId) =>
logRedirectionStrategyMap[sessionId];
static void setLogRedirectionStrategy(
int? sessionId, LogRedirectionStrategy? logRedirectionStrategy) {
if (sessionId != null && logRedirectionStrategy != null) {
logRedirectionStrategyMap[sessionId] = logRedirectionStrategy;
}
}
static LogCallback? getLogCallback(int? sessionId) =>
logCallbackMap[sessionId];
static void setLogCallback(int? sessionId, LogCallback? logCallback) {
if (sessionId != null && logCallback != null) {
logCallbackMap[sessionId] = logCallback;
}
}
static LogCallback? getGlobalLogCallback() => _logCallback;
static void setGlobalLogCallback(LogCallback? logCallback) {
_logCallback = logCallback;
}
static StatisticsCallback? getStatisticsCallback(int? sessionId) =>
statisticsCallbackMap[sessionId];
static void setStatisticsCallback(
int? sessionId, StatisticsCallback? statisticsCallback) {
if (sessionId != null && statisticsCallback != null) {
statisticsCallbackMap[sessionId] = statisticsCallback;
}
}
static StatisticsCallback? getGlobalStatisticsCallback() =>
_statisticsCallback;
static void setGlobalStatisticsCallback(
StatisticsCallback? statisticsCallback) {
_statisticsCallback = statisticsCallback;
}
static FFmpegSessionCompleteCallback? getFFmpegSessionCompleteCallback(
int? sessionId) =>
ffmpegSessionCompleteCallbackMap[sessionId];
static void setFFmpegSessionCompleteCallback(
int? sessionId, FFmpegSessionCompleteCallback? completeCallback) {
if (sessionId != null && completeCallback != null) {
ffmpegSessionCompleteCallbackMap[sessionId] = completeCallback;
}
}
static FFmpegSessionCompleteCallback?
getGlobalFFmpegSessionCompleteCallback() =>
_ffmpegSessionCompleteCallback;
static void setGlobalFFmpegSessionCompleteCallback(
FFmpegSessionCompleteCallback? completeCallback) {
_ffmpegSessionCompleteCallback = completeCallback;
}
static FFprobeSessionCompleteCallback? getFFprobeSessionCompleteCallback(
int? sessionId) =>
ffprobeSessionCompleteCallbackMap[sessionId];
static void setFFprobeSessionCompleteCallback(
int? sessionId, FFprobeSessionCompleteCallback? completeCallback) {
if (sessionId != null && completeCallback != null) {
ffprobeSessionCompleteCallbackMap[sessionId] = completeCallback;
}
}
static FFprobeSessionCompleteCallback?
getGlobalFFprobeSessionCompleteCallback() =>
_ffprobeSessionCompleteCallback;
static void setGlobalFFprobeSessionCompleteCallback(
FFprobeSessionCompleteCallback? completeCallback) {
_ffprobeSessionCompleteCallback = completeCallback;
}
static MediaInformationSessionCompleteCallback?
getMediaInformationSessionCompleteCallback(int? sessionId) =>
mediaInformationSessionCompleteCallbackMap[sessionId];
static void setMediaInformationSessionCompleteCallback(int? sessionId,
MediaInformationSessionCompleteCallback? completeCallback) {
if (sessionId != null && completeCallback != null) {
mediaInformationSessionCompleteCallbackMap[sessionId] = completeCallback;
}
}
static MediaInformationSessionCompleteCallback?
getGlobalMediaInformationSessionCompleteCallback() =>
_mediaInformationSessionCompleteCallback;
static void setGlobalMediaInformationSessionCompleteCallback(
MediaInformationSessionCompleteCallback? completeCallback) {
_mediaInformationSessionCompleteCallback = completeCallback;
}
static DateTime? validDate(int? time) {
if (time == null || time <= 0) {
return null;
} else {
return DateTime.fromMillisecondsSinceEpoch(time, isUtc: false);
}
}
}

View File

@ -0,0 +1,333 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'package:ffmpeg_kit_flutter_platform_interface/ffmpeg_kit_flutter_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../arch_detect.dart';
import '../ffmpeg_kit_config.dart';
import '../ffmpeg_session.dart';
import '../ffmpeg_session_complete_callback.dart';
import '../ffprobe_session.dart';
import '../ffprobe_session_complete_callback.dart';
import '../level.dart';
import '../log_callback.dart';
import '../log_redirection_strategy.dart';
import '../media_information_session.dart';
import '../media_information_session_complete_callback.dart';
import '../packages.dart';
import '../session.dart';
import '../statistics.dart';
import '../statistics_callback.dart';
import 'ffmpeg_kit_factory.dart';
class FFmpegKitInitializer {
static FFmpegKitPlatform _platform = FFmpegKitPlatform.instance;
static const EventChannel _eventChannel =
const EventChannel('flutter.arthenica.com/ffmpeg_kit_event');
static FFmpegKitInitializer _instance = new FFmpegKitInitializer();
static bool _initialized = false;
static Future<bool> initialize() async {
if (!_initialized) {
_initialized = true;
await _instance._initialize();
}
return _initialized;
}
void _onEvent(dynamic event) {
if (event is Map<dynamic, dynamic>) {
final Map<String, dynamic> eventMap = event.cast<String, dynamic>();
final Map<dynamic, dynamic>? logEvent =
eventMap['FFmpegKitLogCallbackEvent'];
final Map<dynamic, dynamic>? statisticsEvent =
eventMap['FFmpegKitStatisticsCallbackEvent'];
final Map<dynamic, dynamic>? completeEvent =
eventMap['FFmpegKitCompleteCallbackEvent'];
if (logEvent != null) {
_processLogCallbackEvent(logEvent);
}
if (statisticsEvent != null) {
_processStatisticsCallbackEvent(statisticsEvent);
}
if (completeEvent != null) {
_processCompleteCallbackEvent(completeEvent);
}
}
}
void _onError(Object error) {
print('Event error: $error');
}
void _processLogCallbackEvent(Map<dynamic, dynamic> event) {
final log = FFmpegKitFactory.mapToLog(event);
final int sessionId = event["sessionId"];
final int level = event["level"];
final String text = event["message"];
final int activeLogLevel = FFmpegKitConfig.getLogLevel();
var globalCallbackDefined = false;
var sessionCallbackDefined = false;
LogRedirectionStrategy activeLogRedirectionStrategy =
FFmpegKitConfig.getLogRedirectionStrategy();
// avLogStderr logs are always redirected
if ((activeLogLevel == Level.avLogQuiet && level != Level.avLogStderr) ||
level > activeLogLevel) {
// LOG NEITHER PRINTED NOR FORWARDED
return;
}
activeLogRedirectionStrategy =
FFmpegKitFactory.getLogRedirectionStrategy(sessionId) ??
activeLogRedirectionStrategy;
final LogCallback? logCallback = FFmpegKitFactory.getLogCallback(sessionId);
if (logCallback != null) {
sessionCallbackDefined = true;
try {
// NOTIFY SESSION CALLBACK DEFINED
logCallback(log);
} on Exception catch (e, stack) {
print("Exception thrown inside session log callback. $e");
print(stack);
}
}
final globalLogCallbackFunction = FFmpegKitFactory.getGlobalLogCallback();
if (globalLogCallbackFunction != null) {
globalCallbackDefined = true;
try {
// NOTIFY GLOBAL CALLBACK DEFINED
globalLogCallbackFunction(log);
} on Exception catch (e, stack) {
print("Exception thrown inside global log callback. $e");
print(stack);
}
}
// EXECUTE THE LOG STRATEGY
switch (activeLogRedirectionStrategy) {
case LogRedirectionStrategy.neverPrintLogs:
{
return;
}
case LogRedirectionStrategy.printLogsWhenGlobalCallbackNotDefined:
{
if (globalCallbackDefined) {
return;
}
}
break;
case LogRedirectionStrategy.printLogsWhenSessionCallbackNotDefined:
{
if (sessionCallbackDefined) {
return;
}
}
break;
case LogRedirectionStrategy.printLogsWhenNoCallbacksDefined:
{
if (globalCallbackDefined || sessionCallbackDefined) {
return;
}
}
break;
case LogRedirectionStrategy.alwaysPrintLogs:
{}
break;
}
// PRINT LOGS
switch (level) {
case Level.avLogQuiet:
{
// PRINT NO OUTPUT
}
break;
default:
{
stdout.write(text);
}
}
}
void _processStatisticsCallbackEvent(Map<dynamic, dynamic> event) {
final Statistics statistics = FFmpegKitFactory.mapToStatistics(event);
final int sessionId = event["sessionId"];
final StatisticsCallback? statisticsCallback =
FFmpegKitFactory.getStatisticsCallback(sessionId);
if (statisticsCallback != null) {
try {
// NOTIFY SESSION CALLBACK DEFINED
statisticsCallback(statistics);
} on Exception catch (e, stack) {
print("Exception thrown inside session statistics callback. $e");
print(stack);
}
}
final globalStatisticsCallbackFunction =
FFmpegKitFactory.getGlobalStatisticsCallback();
if (globalStatisticsCallbackFunction != null) {
try {
// NOTIFY GLOBAL CALLBACK DEFINED
globalStatisticsCallbackFunction(statistics);
} on Exception catch (e, stack) {
print("Exception thrown inside global statistics callback. $e");
print(stack);
}
}
}
void _processCompleteCallbackEvent(Map<dynamic, dynamic> event) {
final int sessionId = event["sessionId"];
FFmpegKitConfig.getSession(sessionId).then((Session? session) {
if (session != null) {
if (session.isFFmpeg()) {
final ffmpegSession = session as FFmpegSession;
final FFmpegSessionCompleteCallback? completeCallback =
ffmpegSession.getCompleteCallback();
if (completeCallback != null) {
try {
// NOTIFY SESSION CALLBACK DEFINED
completeCallback(ffmpegSession);
} on Exception catch (e, stack) {
print("Exception thrown inside session complete callback. $e");
print(stack);
}
}
final globalFFmpegSessionCompleteCallback =
FFmpegKitFactory.getGlobalFFmpegSessionCompleteCallback();
if (globalFFmpegSessionCompleteCallback != null) {
try {
// NOTIFY GLOBAL CALLBACK DEFINED
globalFFmpegSessionCompleteCallback(ffmpegSession);
} on Exception catch (e, stack) {
print("Exception thrown inside global complete callback. $e");
print(stack);
}
}
} else if (session.isFFprobe()) {
final ffprobeSession = session as FFprobeSession;
final FFprobeSessionCompleteCallback? completeCallback =
ffprobeSession.getCompleteCallback();
if (completeCallback != null) {
try {
// NOTIFY SESSION CALLBACK DEFINED
completeCallback(ffprobeSession);
} on Exception catch (e, stack) {
print("Exception thrown inside session complete callback. $e");
print(stack);
}
}
final globalFFprobeSessionCompleteCallback =
FFmpegKitFactory.getGlobalFFprobeSessionCompleteCallback();
if (globalFFprobeSessionCompleteCallback != null) {
try {
// NOTIFY GLOBAL CALLBACK DEFINED
globalFFprobeSessionCompleteCallback(ffprobeSession);
} on Exception catch (e, stack) {
print("Exception thrown inside global complete callback. $e");
print(stack);
}
}
} else if (session.isMediaInformation()) {
final mediaInformationSession = session as MediaInformationSession;
final MediaInformationSessionCompleteCallback? completeCallback =
mediaInformationSession.getCompleteCallback();
if (completeCallback != null) {
try {
// NOTIFY SESSION CALLBACK DEFINED
completeCallback(mediaInformationSession);
} on Exception catch (e, stack) {
print("Exception thrown inside session complete callback. $e");
print(stack);
}
}
final globalMediaInformationSessionCompleteCallback = FFmpegKitFactory
.getGlobalMediaInformationSessionCompleteCallback();
if (globalMediaInformationSessionCompleteCallback != null) {
try {
// NOTIFY GLOBAL CALLBACK DEFINED
globalMediaInformationSessionCompleteCallback(
mediaInformationSession);
} on Exception catch (e, stack) {
print("Exception thrown inside global complete callback. $e");
print(stack);
}
}
}
}
});
}
Future<int?> _getLogLevel() async {
try {
// const MethodChannel _channel =
// const MethodChannel('flutter.arthenica.com/ffmpeg_kit');
// return await _channel.invokeMethod<int>('getLogLevel');
return _platform.ffmpegKitFlutterInitializerGetLogLevel();
} on PlatformException catch (e, stack) {
print("Plugin _getLogLevel error: ${e.message}");
return Future.error("_getLogLevel failed.", stack);
}
}
Future<void> _initialize() async {
debugPrint("Loading ffmpeg-kit-flutter.");
_eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
final logLevel = await _getLogLevel();
if (logLevel != null) {
FFmpegKitConfig.setLogLevel(logLevel);
}
final version = FFmpegKitFactory.getVersion();
final platform = await FFmpegKitConfig.getPlatform();
final arch = await ArchDetect.getArch();
final packageName = await Packages.getPackageName();
await FFmpegKitConfig.enableRedirection();
final isLTSPostfix = (await FFmpegKitConfig.isLTSBuild()) ? "-lts" : "";
final fullVersion = "$platform-$packageName-$arch-$version$isLTSPostfix";
print("Loaded ffmpeg-kit-flutter-$fullVersion.");
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Statistics entry for an FFmpeg execute session.
class Statistics {
int _sessionId;
int _videoFrameNumber;
double _videoFps;
double _videoQuality;
int _size;
int _time;
double _bitrate;
double _speed;
Statistics(this._sessionId, this._videoFrameNumber, this._videoFps,
this._videoQuality, this._size, this._time, this._bitrate, this._speed);
int getSessionId() => this._sessionId;
void setSessionId(int sessionId) {
this._sessionId = sessionId;
}
int getVideoFrameNumber() => this._videoFrameNumber;
void setVideoFrameNumber(int videoFrameNumber) {
this._videoFrameNumber = videoFrameNumber;
}
double getVideoFps() => this._videoFps;
void setVideoFps(double videoFps) {
this._videoFps = videoFps;
}
double getVideoQuality() => this._videoQuality;
void setVideoQuality(double videoQuality) {
this._videoQuality = videoQuality;
}
int getSize() => this._size;
void setSize(int size) {
this._size = size;
}
int getTime() => this._time;
void setTime(int time) {
this._time = time;
}
double getBitrate() => this._bitrate;
void setBitrate(double bitrate) {
this._bitrate = bitrate;
}
double getSpeed() => this._speed;
void setSpeed(double speed) {
this._speed = speed;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019-2021 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
import 'statistics.dart';
/// Callback function that receives statistics generated for "FFmpeg" sessions.
typedef StatisticsCallback = void Function(Statistics statistics);

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) 2019-2022 Taner Sener
*
* This file is part of FFmpegKit.
*
* FFmpegKit is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FFmpegKit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FFmpegKit. If not, see <http://www.gnu.org/licenses/>.
*/
/// Stream information class.
class StreamInformation {
static const keyIndex = "index";
static const keyType = "codec_type";
static const keyCodec = "codec_name";
static const keyCodecLong = "codec_long_name";
static const keyFormat = "pix_fmt";
static const keyWidth = "width";
static const keyHeight = "height";
static const keyBitRate = "bit_rate";
static const keySampleRate = "sample_rate";
static const keySampleFormat = "sample_fmt";
static const keyChannelLayout = "channel_layout";
static const keySampleAspectRatio = "sample_aspect_ratio";
static const keyDisplayAspectRatio = "display_aspect_ratio";
static const keyAverageFrameRate = "avg_frame_rate";
static const keyRealFrameRate = "r_frame_rate";
static const keyTimeBase = "time_base";
static const keyCodecTimeBase = "codec_time_base";
static const keyTags = "tags";
Map<dynamic, dynamic>? _allProperties;
/// Creates a new [StreamInformation] instance
StreamInformation(this._allProperties);
/// Returns stream index.
int? getIndex() => this.getNumberProperty(keyIndex)?.toInt();
/// Returns stream type.
String? getType() => this.getStringProperty(StreamInformation.keyType);
/// Returns stream codec.
String? getCodec() => this.getStringProperty(StreamInformation.keyCodec);
/// Returns stream codec in long format.
String? getFullCodec() =>
this.getStringProperty(StreamInformation.keyCodecLong);
/// Returns stream format.
String? getFormat() => this.getStringProperty(StreamInformation.keyFormat);
/// Returns width.
int? getWidth() =>
this.getNumberProperty(StreamInformation.keyWidth)?.toInt();
/// Returns height.
int? getHeight() =>
this.getNumberProperty(StreamInformation.keyHeight)?.toInt();
/// Returns bitrate.
String? getBitrate() => this.getStringProperty(StreamInformation.keyBitRate);
/// Returns sample rate.
String? getSampleRate() =>
this.getStringProperty(StreamInformation.keySampleRate);
/// Returns sample format.
String? getSampleFormat() =>
this.getStringProperty(StreamInformation.keySampleFormat);
/// Returns channel layout.
String? getChannelLayout() =>
this.getStringProperty(StreamInformation.keyChannelLayout);
/// Returns sample aspect ratio.
String? getSampleAspectRatio() =>
this.getStringProperty(StreamInformation.keySampleAspectRatio);
/// Returns display aspect ratio.
String? getDisplayAspectRatio() =>
this.getStringProperty(StreamInformation.keyDisplayAspectRatio);
/// Returns average frame rate.
String? getAverageFrameRate() =>
this.getStringProperty(StreamInformation.keyAverageFrameRate);
/// Returns real frame rate.
String? getRealFrameRate() =>
this.getStringProperty(StreamInformation.keyRealFrameRate);
/// Returns time base.
String? getTimeBase() =>
this.getStringProperty(StreamInformation.keyTimeBase);
/// Returns codec time base.
String? getCodecTimeBase() =>
this.getStringProperty(StreamInformation.keyCodecTimeBase);
/// Returns all tags.
Map<dynamic, dynamic>? getTags() =>
this.getProperty(StreamInformation.keyTags);
/// Returns the stream property associated with the key.
String? getStringProperty(String key) => this._allProperties?[key];
/// Returns the stream property associated with the key.
num? getNumberProperty(String key) => this._allProperties?[key];
/// Returns the stream property associated with the key.
dynamic getProperty(String key) => this._allProperties?[key];
/// Returns all properties found.
Map<dynamic, dynamic>? getAllProperties() => this._allProperties;
}

View File

@ -0,0 +1,26 @@
name: ffmpeg_kit_flutter_new_min_gpl
description: FFmpeg Kit Min-GPL for Flutter. Supports Android, iOS, macOS and Windows platforms.
repository: https://github.com/sk3llo/ffmpeg_kit_flutter
homepage: https://github.com/sk3llo/ffmpeg_kit_flutter
version: 2.6.2
environment:
sdk: ">=3.1.0 <4.0.0"
flutter: ">=3.0.0"
flutter:
plugin:
platforms:
android:
package: com.antonkarpenko.ffmpegkit
pluginClass: FFmpegKitFlutterPlugin
dependencies:
ffmpeg_kit_flutter_platform_interface: ^0.2.1
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
plugin_platform_interface: ^2.1.8

View File

@ -0,0 +1,119 @@
#!/bin/bash
# Build SPM-consumable combined XCFrameworks (iOS device + iOS simulator + macOS)
# from the ffmpeg-kit release zips of a variant, zip them for use as SPM binary
# targets, and emit their checksums.
#
# Usage: build_spm_artifacts.sh <variant> <version>
# e.g. build_spm_artifacts.sh full-gpl 8.1.2
#
# Inputs: GitHub release assets ffmpeg-kit-{ios,macos}-<variant>-<version>.zip
# under tag <version>-<variant>.
# Outputs: out/<lib>.xcframework.zip (one XCFramework at the zip root, as SPM
# requires) and out/checksums.json, ready to upload to the same release.
#
# The iOS frameworks ship as fat Mach-O bundles (x86_64 sim + arm64/arm64e
# device). The device slice keeps arm64 only (arm64e is dropped, see #164).
# A native arm64 simulator slice is produced by retagging the device
# arm64 slice's LC_BUILD_VERSION platform from iOS (2) to iOS-Simulator (7) —
# FFmpeg is plain C with no platform-conditional linkage, so the retagged slice
# runs natively on Apple Silicon simulators (required by Xcode 26+).
set -euo pipefail
VARIANT="${1:?usage: build_spm_artifacts.sh <variant> <version>}"
VERSION="${2:?usage: build_spm_artifacts.sh <variant> <version>}"
TAG="${VERSION}-${VARIANT}"
BASE="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${TAG}"
FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale"
WORK="spm-artifacts-${TAG}"
mkdir -p "$WORK"
cd "$WORK"
[ -f ios.zip ] || curl -sSL "${BASE}/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip" -o ios.zip
[ -f macos.zip ] || curl -sSL "${BASE}/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip" -o macos.zip
rm -rf ios macos out stage
mkdir -p ios macos out stage
unzip -q -o ios.zip -d ios
unzip -q -o macos.zip -d macos
rm -rf ios/__MACOSX macos/__MACOSX
# Strip bitcode everywhere (no-op if absent)
for FW in $FRAMEWORKS; do
for BIN in "ios/${FW}.framework/${FW}" "macos/${FW}.framework/${FW}"; do
[ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN" || true
done
done
convert() {
local FW="$1"
local IOS_DIR="ios/${FW}.framework"
local IOS_BIN="${IOS_DIR}/${FW}"
local MAC_DIR="macos/${FW}.framework"
local STAGE="stage/${FW}"
rm -rf "$STAGE"
mkdir -p "$STAGE/device" "$STAGE/sim"
local ARCHS MINOS SDK
ARCHS=$(lipo -archs "$IOS_BIN")
MINOS=$(otool -l -arch arm64 "$IOS_BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/minos/{print $2; exit}')
SDK=$(otool -l -arch arm64 "$IOS_BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/sdk/{print $2; exit}')
[ -n "$MINOS" ] || MINOS="14.0"
[ -n "$SDK" ] || SDK="$MINOS"
cp -R "$IOS_DIR" "$STAGE/device/${FW}.framework"
cp -R "$IOS_DIR" "$STAGE/sim/${FW}.framework"
# Device slice: arm64 ONLY. arm64e is deliberately dropped: App Store apps
# run as arm64 (third-party arm64e needs a special entitlement), and App
# Store validation rejects arm64e slices built with pre-iOS-26 SDKs (#164).
lipo "$IOS_BIN" -thin arm64 -output "$STAGE/arm64.bin"
lipo -create "$STAGE/arm64.bin" -output "$STAGE/device/${FW}.framework/${FW}"
# Simulator slice: x86_64 + arm64 retagged iOS(2) -> iOS-Simulator(7)
local SIM_ARGS=()
if echo "$ARCHS" | tr ' ' '\n' | grep -qx "x86_64"; then
lipo "$IOS_BIN" -thin x86_64 -output "$STAGE/x86_64.bin"
SIM_ARGS+=("$STAGE/x86_64.bin")
fi
if echo "$ARCHS" | tr ' ' '\n' | grep -qx "arm64"; then
lipo "$IOS_BIN" -thin arm64 -output "$STAGE/arm64-dev.bin"
vtool -arch arm64 -set-build-version 7 "$MINOS" "$SDK" -replace \
-output "$STAGE/arm64-sim.bin" "$STAGE/arm64-dev.bin"
SIM_ARGS+=("$STAGE/arm64-sim.bin")
fi
lipo -create "${SIM_ARGS[@]}" -output "$STAGE/sim/${FW}.framework/${FW}"
rm -rf "out/${FW}.xcframework"
xcodebuild -create-xcframework \
-framework "$STAGE/device/${FW}.framework" \
-framework "$STAGE/sim/${FW}.framework" \
-framework "$MAC_DIR" \
-output "out/${FW}.xcframework" >/dev/null
# ditto preserves the macOS slice's Versions/ symlinks inside the zip
(cd out && ditto -c -k --keepParent "${FW}.xcframework" "${FW}.xcframework.zip")
rm -rf "$STAGE"
}
for FW in $FRAMEWORKS; do
echo "== $FW"
convert "$FW"
done
rm -rf stage
CK="out/checksums.json"
echo "{" > "$CK"
FIRST=1
for FW in $FRAMEWORKS; do
SUM=$(swift package compute-checksum "out/${FW}.xcframework.zip")
[ $FIRST -eq 1 ] || echo "," >> "$CK"
FIRST=0
printf ' "%s": "%s"' "$FW" "$SUM" >> "$CK"
echo "$FW $SUM"
done
echo "" >> "$CK"
echo "}" >> "$CK"
echo
echo "Artifacts in ${WORK}/out — upload with:"
echo " gh release upload ${TAG} ${WORK}/out/*.xcframework.zip ${WORK}/out/checksums.json"

View File

@ -0,0 +1,201 @@
#!/bin/bash
#
# Downloads the prebuilt FFmpegKit iOS frameworks and installs them into
# ./Frameworks as .xcframeworks. Run from the pod root (the plugin's `ios/`
# directory) — the podspec's prepare_command does exactly that.
#
# Robustness (see issue #88 — "'ffmpegkit/FFmpegKitConfig.h' file not found"):
# * Atomic install: the frameworks appear in ./Frameworks only after a fully
# successful download + extraction + conversion + sanity check. A failure
# never leaves a half-populated ./Frameworks — which is what used to make the
# next `pod install` skip setup and produce a confusing "header not found"
# build error much later.
# * curl uses --fail/--retry so HTTP errors (404, proxy pages) and flaky
# networks fail loudly here instead of silently saving an error page as a zip.
# * The archive is verified before we trust it, and all 8 expected frameworks
# must be present afterwards.
# * Restricted networks (behind a firewall / in a blocked region) can point
# FFMPEG_KIT_IOS_URL at a reachable mirror of the release zip.
#
set -euo pipefail
VERSION="8.1.2"
VARIANT="min-gpl"
DEFAULT_URL="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${VERSION}-${VARIANT}/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip"
IOS_URL="${FFMPEG_KIT_IOS_URL:-$DEFAULT_URL}"
FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale"
# Self-healing / idempotent: if the frameworks are already installed, do nothing.
# The podspec guard checks the same marker, but re-checking here keeps the script
# safe to run manually and safe to re-run after a previous failure.
if [ -d "Frameworks/ffmpegkit.xcframework" ]; then
echo "[ffmpeg_kit_flutter] iOS frameworks already present — skipping download."
exit 0
fi
WORK="$(mktemp -d "${TMPDIR:-/tmp}/ffmpegkit-ios.XXXXXX")"
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
fail() {
{
echo ""
echo "======================================================================"
echo "[ffmpeg_kit_flutter] ERROR: could not set up the iOS frameworks."
echo " $1"
echo ""
echo "The prebuilt FFmpegKit frameworks are downloaded at build time from:"
echo " $IOS_URL"
echo ""
echo "If your network cannot reach GitHub release assets (e.g. behind a"
echo "corporate firewall or in a restricted region), either:"
echo ""
echo " 1. Mirror the zip somewhere reachable and set FFMPEG_KIT_IOS_URL"
echo " before installing pods:"
echo " export FFMPEG_KIT_IOS_URL=\"https://your-mirror/ffmpeg-kit-ios-${VARIANT}-${VERSION}.zip\""
echo " cd ios && pod install"
echo ""
echo " 2. Or download the zip manually and re-run this script from the"
echo " plugin's ios/ directory:"
echo " ../scripts/setup_ios.sh"
echo "======================================================================"
} >&2
exit 1
}
# Build curl args, feature-detecting --retry-all-errors (curl >= 7.71).
CURL_ARGS=(-fL --retry 3 --retry-delay 2 --connect-timeout 30)
if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then
CURL_ARGS+=(--retry-all-errors)
fi
echo "[ffmpeg_kit_flutter] Downloading iOS frameworks ($VARIANT $VERSION)..."
echo "[ffmpeg_kit_flutter] $IOS_URL"
if ! curl "${CURL_ARGS[@]}" -o "$WORK/frameworks.zip" "$IOS_URL"; then
fail "The download failed (see the curl output above)."
fi
# Reject empty / truncated / non-zip payloads before trusting them.
[ -s "$WORK/frameworks.zip" ] || fail "The downloaded file is empty."
if ! unzip -tq "$WORK/frameworks.zip" >/dev/null 2>&1; then
fail "The downloaded file is not a valid zip (likely a partial download or an error page)."
fi
mkdir -p "$WORK/extract"
unzip -oq "$WORK/frameworks.zip" -d "$WORK/extract"
rm -rf "$WORK/extract/__MACOSX"
# Verify the archive actually contained the fat frameworks we expect.
for FW in $FRAMEWORKS; do
[ -d "$WORK/extract/${FW}.framework" ] || \
fail "The archive is missing ${FW}.framework — it may be corrupt or the wrong build."
done
# Delete bitcode from all frameworks.
for FW in $FRAMEWORKS; do
BIN="$WORK/extract/${FW}.framework/${FW}"
[ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN"
done
#
# Convert each fat .framework into an .xcframework that exposes a real
# arm64 iOS-simulator slice.
#
# The released frameworks are fat Mach-O bundles containing:
# x86_64 -> iOS simulator (platform 7)
# arm64 -> iOS device (platform 2)
# arm64e -> iOS device (platform 2)
#
# A fat framework cannot hold both a device-arm64 and a simulator-arm64 slice
# (same CPU type), which is why arm64 had to be excluded for the simulator.
# Apple Silicon simulators on Xcode 26 / iOS 26+ require a native arm64
# simulator slice, so we split each framework into two .xcframework variants:
# ios-arm64 (device; arm64e is dropped, see #164)
# ios-arm64_x86_64-simulator (simulator: x86_64 + arm64 retagged via vtool)
#
# The arm64 simulator slice is produced by retagging the device arm64 slice's
# build-version platform from iOS (2) to iOS-Simulator (7). FFmpeg is plain C
# with no platform-conditional linkage, so the instructions are identical and
# the retagged slice runs natively on Apple Silicon simulators.
#
# BASE is the directory holding the fat .framework inputs; the resulting
# .xcframework is written alongside them and later moved into ./Frameworks.
#
convert_to_xcframework() {
local FW="$1"
local BASE="$2"
local DIR="${BASE}/${FW}.framework"
local BIN="${DIR}/${FW}"
[ -d "$DIR" ] || return 0
local ARCHS
ARCHS=$(lipo -archs "$BIN")
# Read the device deployment target / sdk so vtool reproduces them.
local MINOS SDK
MINOS=$(otool -l -arch arm64 "$BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/minos/{print $2; exit}')
SDK=$(otool -l -arch arm64 "$BIN" 2>/dev/null | awk '/LC_BUILD_VERSION/{b=1} b&&/sdk/{print $2; exit}')
[ -n "$MINOS" ] || MINOS="14.0"
[ -n "$SDK" ] || SDK="$MINOS"
local STAGE="${BASE}/.xc_tmp/${FW}"
rm -rf "$STAGE"
mkdir -p "$STAGE"
cp -R "$DIR" "$STAGE/device.framework"
cp -R "$DIR" "$STAGE/sim.framework"
# xcodebuild requires the inner framework names to match the binary name.
mv "$STAGE/device.framework" "$STAGE/device/${FW}.framework" 2>/dev/null || { mkdir -p "$STAGE/device"; mv "$STAGE/device.framework" "$STAGE/device/${FW}.framework"; }
mkdir -p "$STAGE/sim"
mv "$STAGE/sim.framework" "$STAGE/sim/${FW}.framework"
# --- Device slice: arm64 ONLY. arm64e is deliberately dropped: App Store
# apps run as arm64 (third-party arm64e needs a special entitlement), and
# App Store validation rejects arm64e slices built with pre-iOS-26 SDKs (#164).
lipo "$BIN" -thin arm64 -output "$STAGE/arm64.dylib"
lipo -create "$STAGE/arm64.dylib" -output "$STAGE/device/${FW}.framework/${FW}"
# --- Simulator slice: x86_64 (+ arm64 retagged to simulator) ---
local SIM_ARGS=()
if echo "$ARCHS" | tr ' ' '\n' | grep -qx "x86_64"; then
lipo "$BIN" -thin x86_64 -output "$STAGE/x86_64.dylib"
SIM_ARGS+=("$STAGE/x86_64.dylib")
fi
if echo "$ARCHS" | tr ' ' '\n' | grep -qx "arm64"; then
lipo "$BIN" -thin arm64 -output "$STAGE/arm64-dev.dylib"
vtool -arch arm64 -set-build-version 7 "$MINOS" "$SDK" -replace \
-output "$STAGE/arm64-sim.dylib" "$STAGE/arm64-dev.dylib"
SIM_ARGS+=("$STAGE/arm64-sim.dylib")
fi
lipo -create "${SIM_ARGS[@]}" -output "$STAGE/sim/${FW}.framework/${FW}"
# --- Assemble the xcframework ---
rm -rf "${BASE}/${FW}.xcframework"
xcodebuild -create-xcframework \
-framework "$STAGE/device/${FW}.framework" \
-framework "$STAGE/sim/${FW}.framework" \
-output "${BASE}/${FW}.xcframework"
rm -rf "$DIR" "$STAGE"
}
echo "[ffmpeg_kit_flutter] Building xcframeworks..."
for FW in $FRAMEWORKS; do
convert_to_xcframework "$FW" "$WORK/extract"
done
rm -rf "$WORK/extract/.xc_tmp"
# Verify every xcframework was produced before we touch ./Frameworks.
for FW in $FRAMEWORKS; do
[ -d "$WORK/extract/${FW}.xcframework" ] || \
fail "Failed to build ${FW}.xcframework (Xcode command line tools required)."
done
# --- Atomic install: only now do we populate ./Frameworks ---
rm -rf Frameworks
mkdir -p Frameworks
for FW in $FRAMEWORKS; do
mv "$WORK/extract/${FW}.xcframework" "Frameworks/${FW}.xcframework"
done
echo "[ffmpeg_kit_flutter] iOS frameworks installed successfully."

View File

@ -0,0 +1,93 @@
#!/bin/bash
#
# Downloads the prebuilt FFmpegKit macOS frameworks and installs them into
# ./Frameworks. Run from the pod root (the plugin's `macos/` directory) — the
# podspec's prepare_command does exactly that.
#
# Same robustness contract as setup_ios.sh (see issue #88):
# * Atomic install — ./Frameworks is only populated after a fully successful
# download + extraction + sanity check, so a failure never leaves a broken
# half-populated directory behind (which is what made the next `pod install`
# skip setup and produce a confusing "header not found" build error).
# * curl --fail/--retry surfaces network errors here instead of later.
# * FFMPEG_KIT_MACOS_URL overrides the download URL for restricted networks.
#
set -euo pipefail
VERSION="8.1.2"
VARIANT="min-gpl"
DEFAULT_URL="https://github.com/sk3llo/ffmpeg_kit_flutter/releases/download/${VERSION}-${VARIANT}/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip"
MACOS_URL="${FFMPEG_KIT_MACOS_URL:-$DEFAULT_URL}"
FRAMEWORKS="ffmpegkit libavcodec libavdevice libavfilter libavformat libavutil libswresample libswscale"
# Self-healing / idempotent.
if [ -d "Frameworks/ffmpegkit.framework" ]; then
echo "[ffmpeg_kit_flutter] macOS frameworks already present — skipping download."
exit 0
fi
WORK="$(mktemp -d "${TMPDIR:-/tmp}/ffmpegkit-macos.XXXXXX")"
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
fail() {
{
echo ""
echo "======================================================================"
echo "[ffmpeg_kit_flutter] ERROR: could not set up the macOS frameworks."
echo " $1"
echo ""
echo "The prebuilt FFmpegKit frameworks are downloaded at build time from:"
echo " $MACOS_URL"
echo ""
echo "If your network cannot reach GitHub release assets, either mirror the"
echo "zip and set FFMPEG_KIT_MACOS_URL before installing pods:"
echo " export FFMPEG_KIT_MACOS_URL=\"https://your-mirror/ffmpeg-kit-macos-${VARIANT}-${VERSION}.zip\""
echo " cd macos && pod install"
echo "or download it manually and re-run ../scripts/setup_macos.sh."
echo "======================================================================"
} >&2
exit 1
}
CURL_ARGS=(-fL --retry 3 --retry-delay 2 --connect-timeout 30)
if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then
CURL_ARGS+=(--retry-all-errors)
fi
echo "[ffmpeg_kit_flutter] Downloading macOS frameworks ($VARIANT $VERSION)..."
echo "[ffmpeg_kit_flutter] $MACOS_URL"
if ! curl "${CURL_ARGS[@]}" -o "$WORK/frameworks.zip" "$MACOS_URL"; then
fail "The download failed (see the curl output above)."
fi
[ -s "$WORK/frameworks.zip" ] || fail "The downloaded file is empty."
if ! unzip -tq "$WORK/frameworks.zip" >/dev/null 2>&1; then
fail "The downloaded file is not a valid zip (likely a partial download or an error page)."
fi
mkdir -p "$WORK/extract"
unzip -oq "$WORK/frameworks.zip" -d "$WORK/extract"
rm -rf "$WORK/extract/__MACOSX"
# Verify all expected frameworks are present.
for FW in $FRAMEWORKS; do
[ -d "$WORK/extract/${FW}.framework" ] || \
fail "The archive is missing ${FW}.framework — it may be corrupt or the wrong build."
done
# Delete bitcode from all frameworks (required for App Store submission).
for FW in $FRAMEWORKS; do
BIN="$WORK/extract/${FW}.framework/${FW}"
[ -f "$BIN" ] && xcrun bitcode_strip -r "$BIN" -o "$BIN"
done
# --- Atomic install ---
rm -rf Frameworks
mkdir -p Frameworks
for FW in $FRAMEWORKS; do
mv "$WORK/extract/${FW}.framework" "Frameworks/${FW}.framework"
done
echo "[ffmpeg_kit_flutter] macOS frameworks installed successfully."

View File

@ -0,0 +1,103 @@
## 1.1.3
- Updated flutter_inappwebview_platform_interface version to ^1.3.0
## 1.1.2
- Removed webview/plugin_scripts_js/ConsoleLogJS.java file, use native WebChromeClient.onConsoleMessage instead
## 1.1.1
- Updated flutter_inappwebview_platform_interface version to ^1.2.0
## 1.1.0+4
- Updated flutter_inappwebview_platform_interface version
## 1.1.0+3
- Fixed compilation error
## 1.1.0+2
- Updated pubspec.yaml
## 1.1.0+1
- Downgraded androidx.appcompat:appcompat:1.7.0 to androidx.appcompat:appcompat:1.6.1
- Added `-dontwarn android.window.BackEvent` proguard rule
## 1.1.0
- Updated androidx.webkit:webkit:1.8.0 to androidx.webkit:webkit:1.12.0
- Updated androidx.browser:browser:1.6.0 to androidx.browser:browser:1.8.0
- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5.
- Removed unsupported WebViewFeature.SUPPRESS_ERROR_PAGE
- Merged "Remove references to deprecated v1 Android embedding" [#2176](https://github.com/pichillilorenzo/flutter_inappwebview/pull/2176) (thanks to [gmackall](https://github.com/gmackall))
## 1.0.13
- Fixed "Android emulator using API 34 fails to draw on resume sometimes" [#1981](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1981)
## 1.0.12
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10`
## 1.0.11
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9`
- Fix typos (thanks to [michalsrutek](https://github.com/michalsrutek))
## 1.0.10
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8`
- Implemented `PlatformCustomPathHandler` class
## 1.0.9
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7`
- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738)
- Fixed `InAppWebViewController.callAsyncJavaScript` issue when the last line of the `functionBody` parameter includes a code comment
## 1.0.8
- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests`
- Implemented `PlatformInAppWebViewController.clearFormData` method
- Implemented `PlatformCookieManager.removeSessionCookies` method
- Updated `useShouldInterceptAjaxRequest` automatic infer logic
- Updated `CookieManager` methods return value
## 1.0.7
- Merged "Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property'" [#1904](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1904) (thanks to [EArminjon](https://github.com/EArminjon))
- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters
## 1.0.6
- Fixed "getFavicons: _TypeError: type '_Map<String, dynamic>' is not a subtype of type 'Iterable<dynamic>'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897)
- Fixed "onClosed not considering back navigation or up button / close button in ChromeSafariBrowser when using noHistory: true" [#1882](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1882)
## 1.0.5
- Call `super.dispose();` on `InAppBrowser` and `ChromeSafari` implementations
## 1.0.4
- Throw platform exception when ProcessGlobalConfig.apply throws an error on the native side to be able to catch it on Flutter side
## 1.0.3
- Updated `ContentBlockerHandler` CSS_DISPLAY_NONE action type and `JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE` javascript implementation code
## 1.0.2
- Updated `flutter_inappwebview_platform_interface` version dependency to `1.0.2`
- Fixed "Crash when starting ChromeSafariBrowser on Android java.lang.NoSuchMethodError: No virtual method isEngagementSignalsApiAvailable" [#1881](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1881)
## 1.0.1
- Updated README
## 1.0.0
Initial release.

View File

@ -0,0 +1,10 @@
# flutter_inappwebview_android 1.1.3 사본 (JJ_MKVMaker 패치)
원본: https://pub.dev/packages/flutter_inappwebview_android 1.1.3 (pubspec.yaml 의 dependency_overrides 로 사용)
## 고친 문제
새 Android 빌드 도구 (AGP 9) 가 `getDefaultProguardFile('proguard-android.txt')` 를 더는 받지 않아 APK 빌드가 실패함.
## 수정
- android/build.gradle: `proguard-android.txt` → `proguard-android-optimize.txt`
- example 폴더 제거

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 Lorenzo Pichilli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,13 @@
# flutter\_inappwebview\_android
The Android WebView implementation of [`flutter_inappwebview`](https://pub.dev/packages/flutter_inappwebview).
## Usage
This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin),
which means you can simply use `flutter_inappwebview`
normally. This package will be automatically included in your app when you do,
so you do not need to add it to your `pubspec.yaml`.
However, if you `import` this package to use any of its APIs directly, you
should add it to your `pubspec.yaml` as usual.

View File

@ -0,0 +1,15 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
constant_identifier_names: ignore
deprecated_member_use_from_same_package: ignore
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
errors:
deprecated_member_use: ignore
deprecated_member_use_from_same_package: ignore
unnecessary_cast: ignore
unnecessary_import: ignore

View File

@ -0,0 +1,57 @@
group 'com.pichillilorenzo.flutter_inappwebview_android'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.1'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
// Conditional for compatibility with AGP <4.2.
if (project.android.hasProperty("namespace")) {
namespace 'com.pichillilorenzo.flutter_inappwebview_android'
}
compileSdk 34
defaultConfig {
minSdkVersion 19
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
consumerProguardFiles 'proguard-rules.pro'
}
lintOptions {
disable 'InvalidPackage'
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
dependencies {
implementation 'androidx.webkit:webkit:1.12.0'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,19 @@
# WebView
-keepattributes *JavascriptInterface*
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
public void *(android.webkit.webView, jav.lang.String);
}
-keepclassmembers class com.pichillilorenzo.flutter_inappwebview_android$JavaScriptBridgeInterface {
<fields>;
<methods>;
public *;
private *;
}
-keep class com.pichillilorenzo.flutter_inappwebview_android.** { *; }
-dontwarn android.window.BackEvent

View File

@ -0,0 +1 @@
rootProject.name = 'flutter_inappwebview_android'

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pichillilorenzo.flutter_inappwebview_android">
<application>
<activity
android:theme="@style/AppTheme"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivity" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivity" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivitySingleInstance"
android:launchMode="singleInstance"/>
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivitySingleInstance"
android:launchMode="singleInstance"/>
<receiver
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ActionBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
</application>
<queries>
<intent>
<action android:name=
"android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,11 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.annotation.NonNull;
import java.util.Map;
public interface ISettings<T> {
@NonNull ISettings<T> parse(@NonNull Map<String, Object> settings);
@NonNull Map<String, Object> toMap();
@NonNull Map<String, Object> getRealSettings(@NonNull T obj);
}

View File

@ -0,0 +1,9 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.core.content.FileProvider;
public class InAppWebViewFileProvider extends FileProvider {
public static final String fileProviderAuthorityExtension = "flutter_inappwebview_android.fileprovider";
}

View File

@ -0,0 +1,224 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeSafariBrowserManager;
import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.NoHistoryCustomTabsActivityCallbacks;
import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabaseHandler;
import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebViewManager;
import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserManager;
import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobManager;
import com.pichillilorenzo.flutter_inappwebview_android.process_global_config.ProcessGlobalConfigManager;
import com.pichillilorenzo.flutter_inappwebview_android.proxy.ProxyManager;
import com.pichillilorenzo.flutter_inappwebview_android.service_worker.ServiceWorkerManager;
import com.pichillilorenzo.flutter_inappwebview_android.tracing.TracingControllerManager;
import com.pichillilorenzo.flutter_inappwebview_android.webview.FlutterWebViewFactory;
import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewManager;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.platform.PlatformViewRegistry;
import io.flutter.embedding.android.FlutterView;
public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
protected static final String LOG_TAG = "InAppWebViewFlutterPL";
@Nullable
public PlatformUtil platformUtil;
@Nullable
public InAppBrowserManager inAppBrowserManager;
@Nullable
public HeadlessInAppWebViewManager headlessInAppWebViewManager;
@Nullable
public ChromeSafariBrowserManager chromeSafariBrowserManager;
@Nullable
public NoHistoryCustomTabsActivityCallbacks noHistoryCustomTabsActivityCallbacks;
@Nullable
public InAppWebViewManager inAppWebViewManager;
@Nullable
public MyCookieManager myCookieManager;
@Nullable
public CredentialDatabaseHandler credentialDatabaseHandler;
@Nullable
public MyWebStorage myWebStorage;
@Nullable
public ServiceWorkerManager serviceWorkerManager;
@Nullable
public WebViewFeatureManager webViewFeatureManager;
@Nullable
public ProxyManager proxyManager;
@Nullable
public PrintJobManager printJobManager;
@Nullable
public TracingControllerManager tracingControllerManager;
@Nullable
public ProcessGlobalConfigManager processGlobalConfigManager;
public FlutterWebViewFactory flutterWebViewFactory;
public Context applicationContext;
public BinaryMessenger messenger;
public FlutterPlugin.FlutterAssets flutterAssets;
@Nullable
public ActivityPluginBinding activityPluginBinding;
@Nullable
public Activity activity;
public FlutterView flutterView;
public InAppWebViewFlutterPlugin() {}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
this.flutterAssets = binding.getFlutterAssets();
// Shared.activity could be null or not.
// It depends on who is called first between onAttachedToEngine event and onAttachedToActivity event.
//
// See https://github.com/pichillilorenzo/flutter_inappwebview/issues/390#issuecomment-647039084
onAttachedToEngine(
binding.getApplicationContext(), binding.getBinaryMessenger(), this.activity, binding.getPlatformViewRegistry(), null);
}
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger, Activity activity, PlatformViewRegistry platformViewRegistry, FlutterView flutterView) {
this.applicationContext = applicationContext;
this.activity = activity;
this.messenger = messenger;
this.flutterView = flutterView;
inAppBrowserManager = new InAppBrowserManager(this);
headlessInAppWebViewManager = new HeadlessInAppWebViewManager(this);
chromeSafariBrowserManager = new ChromeSafariBrowserManager(this);
noHistoryCustomTabsActivityCallbacks = new NoHistoryCustomTabsActivityCallbacks(this);
flutterWebViewFactory = new FlutterWebViewFactory(this);
platformViewRegistry.registerViewFactory(
FlutterWebViewFactory.VIEW_TYPE_ID, flutterWebViewFactory);
platformUtil = new PlatformUtil(this);
inAppWebViewManager = new InAppWebViewManager(this);
myCookieManager = new MyCookieManager(this);
myWebStorage = new MyWebStorage(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
serviceWorkerManager = new ServiceWorkerManager(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
credentialDatabaseHandler = new CredentialDatabaseHandler(this);
}
webViewFeatureManager = new WebViewFeatureManager(this);
proxyManager = new ProxyManager(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
printJobManager = new PrintJobManager(this);
}
tracingControllerManager = new TracingControllerManager(this);
processGlobalConfigManager = new ProcessGlobalConfigManager(this);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
if (platformUtil != null) {
platformUtil.dispose();
platformUtil = null;
}
if (inAppBrowserManager != null) {
inAppBrowserManager.dispose();
inAppBrowserManager = null;
}
if (headlessInAppWebViewManager != null) {
headlessInAppWebViewManager.dispose();
headlessInAppWebViewManager = null;
}
if (chromeSafariBrowserManager != null) {
chromeSafariBrowserManager.dispose();
chromeSafariBrowserManager = null;
}
if (noHistoryCustomTabsActivityCallbacks != null) {
noHistoryCustomTabsActivityCallbacks.dispose();
noHistoryCustomTabsActivityCallbacks = null;
}
if (myCookieManager != null) {
myCookieManager.dispose();
myCookieManager = null;
}
if (myWebStorage != null) {
myWebStorage.dispose();
myWebStorage = null;
}
if (credentialDatabaseHandler != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
credentialDatabaseHandler.dispose();
credentialDatabaseHandler = null;
}
if (inAppWebViewManager != null) {
inAppWebViewManager.dispose();
inAppWebViewManager = null;
}
if (serviceWorkerManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
serviceWorkerManager.dispose();
serviceWorkerManager = null;
}
if (webViewFeatureManager != null) {
webViewFeatureManager.dispose();
webViewFeatureManager = null;
}
if (proxyManager != null) {
proxyManager.dispose();
proxyManager = null;
}
if (printJobManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
printJobManager.dispose();
printJobManager = null;
}
if (tracingControllerManager != null) {
tracingControllerManager.dispose();
tracingControllerManager = null;
}
if (processGlobalConfigManager != null) {
processGlobalConfigManager.dispose();
processGlobalConfigManager = null;
}
}
@Override
public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
this.activityPluginBinding = activityPluginBinding;
this.activity = activityPluginBinding.getActivity();
if (noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
}
@Override
public void onDetachedFromActivityForConfigChanges() {
if (activity != null && noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
activityPluginBinding = null;
activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
this.activityPluginBinding = activityPluginBinding;
this.activity = activityPluginBinding.getActivity();
if (noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
}
@Override
public void onDetachedFromActivity() {
if (activity != null && noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
activityPluginBinding = null;
activity = null;
}
}

View File

@ -0,0 +1,437 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.os.Build;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.ValueCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.CookieManagerCompat;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MyCookieManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "MyCookieManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager";
@Nullable
public static CookieManager cookieManager;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public MyCookieManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init() {
if (cookieManager == null) {
cookieManager = getCookieManager();
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
init();
switch (call.method) {
case "setCookie":
{
String url = (String) call.argument("url");
String name = (String) call.argument("name");
String value = (String) call.argument("value");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
String expiresDateString = (String) call.argument("expiresDate");
Long expiresDate = (expiresDateString != null ? new Long(expiresDateString) : null);
Integer maxAge = (Integer) call.argument("maxAge");
Boolean isSecure = (Boolean) call.argument("isSecure");
Boolean isHttpOnly = (Boolean) call.argument("isHttpOnly");
String sameSite = (String) call.argument("sameSite");
setCookie(url,
name,
value,
domain,
path,
expiresDate,
maxAge,
isSecure,
isHttpOnly,
sameSite,
result);
}
break;
case "getCookies":
result.success(getCookies((String) call.argument("url")));
break;
case "deleteCookie":
{
String url = (String) call.argument("url");
String name = (String) call.argument("name");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
deleteCookie(url, name, domain, path, result);
}
break;
case "deleteCookies":
{
String url = (String) call.argument("url");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
deleteCookies(url, domain, path, result);
}
break;
case "deleteAllCookies":
deleteAllCookies(result);
break;
case "removeSessionCookies":
removeSessionCookies(result);
break;
default:
result.notImplemented();
}
}
/**
* Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily
* to make sure it's done on a background thread as needed.
*
* https://github.com/facebook/react-native/blob/1903f6680d9750e244d97c3cd4a9f755a9a47c61/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java#L132
*/
static private @Nullable CookieManager getCookieManager() {
if (cookieManager == null) {
try {
cookieManager = CookieManager.getInstance();
} catch (IllegalArgumentException ex) {
// https://bugs.chromium.org/p/chromium/issues/detail?id=559720
return null;
} catch (Exception exception) {
String message = exception.getMessage();
// We cannot catch MissingWebViewPackageException as it is in a private / system API
// class. This validates the exception's message to ensure we are only handling this
// specific exception.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewFactory.java#348
if (message != null
&& exception
.getClass()
.getCanonicalName()
.equals("android.webkit.WebViewFactory.MissingWebViewPackageException")) {
return null;
} else {
throw exception;
}
}
}
return cookieManager;
}
public void setCookie(String url,
String name,
String value,
String domain,
String path,
Long expiresDate,
Integer maxAge,
Boolean isSecure,
Boolean isHttpOnly,
String sameSite,
final MethodChannel.Result result) {
cookieManager = getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
String cookieValue = name + "=" + value + "; Path=" + path;
if (domain != null)
cookieValue += "; Domain=" + domain;
if (expiresDate != null)
cookieValue += "; Expires=" + getCookieExpirationDate(expiresDate);
if (maxAge != null)
cookieValue += "; Max-Age=" + maxAge.toString();
if (isSecure != null && isSecure)
cookieValue += "; Secure";
if (isHttpOnly != null && isHttpOnly)
cookieValue += "; HttpOnly";
if (sameSite != null)
cookieValue += "; SameSite=" + sameSite;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.setCookie(url, cookieValue);
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.setCookie(url, cookieValue);
result.success(true);
}
}
public List<Map<String, Object>> getCookies(final String url) {
final List<Map<String, Object>> cookieListMap = new ArrayList<>();
cookieManager = getCookieManager();
if (cookieManager == null) return cookieListMap;
List<String> cookies = new ArrayList<>();
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
cookies = CookieManagerCompat.getCookieInfo(cookieManager, url);
} else {
String cookiesString = cookieManager.getCookie(url);
if (cookiesString != null) {
cookies = Arrays.asList(cookiesString.split(";"));
}
}
for (String cookie : cookies) {
String[] cookieParams = cookie.split(";");
if (cookieParams.length == 0) continue;
String[] nameValue = cookieParams[0].split("=", 2);
String name = nameValue[0].trim();
String value = (nameValue.length > 1) ? nameValue[1].trim() : "";
Map<String, Object> cookieMap = new HashMap<>();
cookieMap.put("name", name);
cookieMap.put("value", value);
cookieMap.put("expiresDate", null);
cookieMap.put("isSessionOnly", null);
cookieMap.put("domain", null);
cookieMap.put("sameSite", null);
cookieMap.put("isSecure", null);
cookieMap.put("isHttpOnly", null);
cookieMap.put("path", null);
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
cookieMap.put("isSecure", false);
cookieMap.put("isHttpOnly", false);
for (int i = 1; i < cookieParams.length; i++) {
String[] cookieParamNameValue = cookieParams[i].split("=", 2);
String cookieParamName = cookieParamNameValue[0].trim();
String cookieParamValue = (cookieParamNameValue.length > 1) ? cookieParamNameValue[1].trim() : "";
if (cookieParamName.equalsIgnoreCase("Expires")) {
try {
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
Date expiryDate = sdf.parse(cookieParamValue);
if (expiryDate != null) {
cookieMap.put("expiresDate", expiryDate.getTime());
}
} catch (ParseException e) {
Log.e(LOG_TAG, "", e);
}
} else if (cookieParamName.equalsIgnoreCase("Max-Age")) {
try {
long maxAge = Long.parseLong(cookieParamValue);
cookieMap.put("expiresDate", System.currentTimeMillis() + maxAge);
} catch (NumberFormatException e) {
Log.e(LOG_TAG, "", e);
}
} else if (cookieParamName.equalsIgnoreCase("Domain")) {
cookieMap.put("domain", cookieParamValue);
} else if (cookieParamName.equalsIgnoreCase("SameSite")) {
cookieMap.put("sameSite", cookieParamValue);
} else if (cookieParamName.equalsIgnoreCase("Secure")) {
cookieMap.put("isSecure", true);
} else if (cookieParamName.equalsIgnoreCase("HttpOnly")) {
cookieMap.put("isHttpOnly", true);
} else if (cookieParamName.equalsIgnoreCase("Path")) {
cookieMap.put("path", cookieParamValue);
}
}
}
cookieListMap.add(cookieMap);
}
return cookieListMap;
}
public void deleteCookie(String url, String name, String domain, String path, final MethodChannel.Result result) {
cookieManager = getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
if (domain != null)
cookieValue += "; Domain=" + domain;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.setCookie(url, cookieValue);
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.setCookie(url, cookieValue);
result.success(true);
}
}
public void deleteCookies(String url, String domain, String path, final MethodChannel.Result result) {
cookieManager = getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
CookieSyncManager cookieSyncMngr = null;
String cookiesString = cookieManager.getCookie(url);
if (cookiesString != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && plugin != null) {
cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
}
String[] cookies = cookiesString.split(";");
for (String cookie : cookies) {
String[] nameValue = cookie.split("=", 2);
String name = nameValue[0].trim();
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
if (domain != null)
cookieValue += "; Domain=" + domain;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
cookieManager.setCookie(url, cookieValue, null);
else
cookieManager.setCookie(url, cookieValue);
}
if (cookieSyncMngr != null) {
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
cookieManager.flush();
}
result.success(true);
}
public void deleteAllCookies(final MethodChannel.Result result) {
cookieManager = getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.removeAllCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.removeAllCookie();
result.success(true);
}
}
public void removeSessionCookies(final MethodChannel.Result result) {
cookieManager = getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeSessionCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.removeSessionCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.removeSessionCookie();
result.success(true);
}
}
public static String getCookieExpirationDate(Long timestamp) {
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(new Date(timestamp));
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}

View File

@ -0,0 +1,138 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.webkit.ValueCallback;
import android.webkit.WebStorage;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MyWebStorage extends ChannelDelegateImpl {
protected static final String LOG_TAG = "MyWebStorage";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager";
@Nullable
public static WebStorage webStorageManager;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public MyWebStorage(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init() {
if (webStorageManager == null) {
webStorageManager = WebStorage.getInstance();
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
init();
switch (call.method) {
case "getOrigins":
getOrigins(result);
break;
case "deleteAllData":
if (webStorageManager != null) {
webStorageManager.deleteAllData();
result.success(true);
} else {
result.success(false);
}
break;
case "deleteOrigin":
{
if (webStorageManager != null) {
String origin = (String) call.argument("origin");
webStorageManager.deleteOrigin(origin);
result.success(true);
} else {
result.success(false);
}
}
break;
case "getQuotaForOrigin":
{
String origin = (String) call.argument("origin");
getQuotaForOrigin(origin, result);
}
break;
case "getUsageForOrigin":
{
String origin = (String) call.argument("origin");
getUsageForOrigin(origin, result);
}
break;
default:
result.notImplemented();
}
}
public void getOrigins(final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(new ArrayList<>());
return;
}
webStorageManager.getOrigins(new ValueCallback<Map>() {
@Override
public void onReceiveValue(Map value) {
List<Map<String, Object>> origins = new ArrayList<>();
for(Object key : value.keySet()) {
WebStorage.Origin originObj = (WebStorage.Origin) value.get(key);
Map<String, Object> originInfo = new HashMap<>();
originInfo.put("origin", originObj.getOrigin());
originInfo.put("quota", originObj.getQuota());
originInfo.put("usage", originObj.getUsage());
origins.add(originInfo);
}
result.success(origins);
}
});
}
public void getQuotaForOrigin(String origin, final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(0);
return;
}
webStorageManager.getQuotaForOrigin(origin, new ValueCallback<Long>() {
@Override
public void onReceiveValue(Long value) {
result.success(value);
}
});
}
public void getUsageForOrigin(String origin, final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(0);
return;
}
webStorageManager.getUsageForOrigin(origin, new ValueCallback<Long>() {
@Override
public void onReceiveValue(Long value) {
result.success(value);
}
});
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}

View File

@ -0,0 +1,73 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class PlatformUtil extends ChannelDelegateImpl {
protected static final String LOG_TAG = "PlatformUtil";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public PlatformUtil(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "getSystemVersion":
result.success(String.valueOf(Build.VERSION.SDK_INT));
break;
case "formatDate":
long date = (long) call.argument("date");
String format = (String) call.argument("format");
Locale locale = PlatformUtil.getLocaleFromString((String) call.argument("locale"));
String timezone = (String) call.argument("timezone");
if (timezone == null) {
timezone = "UTC";
}
result.success(PlatformUtil.formatDate(date, format, locale, TimeZone.getTimeZone(timezone)));
break;
default:
result.notImplemented();
}
}
public static Locale getLocaleFromString(@Nullable String locale) {
if (locale == null) {
return Locale.US;
}
String[] localeSplit = locale.split("_");
String language = localeSplit[0];
String country = localeSplit.length > 1 ? localeSplit[1] : "";
String variant = localeSplit.length > 2 ? localeSplit[2] : "";
return new Locale(language, country, variant);
}
public static String formatDate(long date, String format, Locale locale, TimeZone timezone) {
final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
sdf.setTimeZone(timezone);
return sdf.format(new Date(date));
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}

View File

@ -0,0 +1,386 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.BitmapFactory;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.http.SslCertificate;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.net.ssl.SSLHandshakeException;
import io.flutter.plugin.common.MethodChannel;
public class Util {
static final String LOG_TAG = "Util";
public static final String ANDROID_ASSET_URL = "file:///android_asset/";
private Util() {}
public static String getUrlAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
InputStream is = null;
IOException e = null;
try {
is = getFileAsset(plugin, assetFilePath);
} catch (IOException ex) {
e = ex;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ex) {
e = ex;
}
}
}
if (e != null) {
throw e;
}
return ANDROID_ASSET_URL + key;
}
public static InputStream getFileAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
AssetManager mg = plugin.applicationContext.getResources().getAssets();
return mg.open(key);
}
public static <T> T invokeMethodAndWaitResult(final @NonNull MethodChannel channel,
final @NonNull String method, final @Nullable Object arguments,
final @NonNull SyncBaseCallbackResultImpl<T> callback) throws InterruptedException {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
channel.invokeMethod(method, arguments, callback);
}
});
callback.latch.await();
return callback.result;
}
@Nullable
public static PrivateKeyAndCertificates loadPrivateKeyAndCertificate(@NonNull InAppWebViewFlutterPlugin plugin,
@NonNull String certificatePath,
@Nullable String certificatePassword,
@NonNull String keyStoreType) {
PrivateKeyAndCertificates privateKeyAndCertificates = null;
InputStream certificateFileStream = null;
try {
certificateFileStream = getFileAsset(plugin, certificatePath);
} catch (IOException ignored) {}
try {
if (certificateFileStream == null) {
certificateFileStream = new FileInputStream(certificatePath);
}
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(certificateFileStream, (certificatePassword != null ? certificatePassword : "").toCharArray());
Enumeration<String> aliases = keyStore.aliases();
String alias = aliases.nextElement();
Key key = keyStore.getKey(alias, (certificatePassword != null ? certificatePassword : "").toCharArray());
if (key instanceof PrivateKey) {
PrivateKey privateKey = (PrivateKey)key;
Certificate cert = keyStore.getCertificate(alias);
X509Certificate[] certificates = new X509Certificate[1];
certificates[0] = (X509Certificate)cert;
privateKeyAndCertificates = new PrivateKeyAndCertificates(privateKey, certificates);
}
certificateFileStream.close();
} catch (Exception e) {
Log.e(LOG_TAG, "", e);
} finally {
if (certificateFileStream != null) {
try {
certificateFileStream.close();
} catch (IOException ex) {
Log.e(LOG_TAG, "", ex);
}
}
}
return privateKeyAndCertificates;
}
public static class PrivateKeyAndCertificates {
public X509Certificate[] certificates;
public PrivateKey privateKey;
public PrivateKeyAndCertificates(PrivateKey privateKey, X509Certificate[] certificates) {
this.privateKey = privateKey;
this.certificates = certificates;
}
}
@Nullable
public static HttpURLConnection makeHttpRequest(String urlString, String method, @Nullable Map<String, String> headers) {
HttpURLConnection urlConnection = null;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod(method);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
urlConnection.setRequestProperty(header.getKey(), header.getValue());
}
}
urlConnection.setConnectTimeout(15000); // 15 seconds
urlConnection.setReadTimeout(15000); // 15 seconds
urlConnection.setDoInput(true);
urlConnection.setInstanceFollowRedirects(true);
if ("GET".equalsIgnoreCase(method)) {
urlConnection.setDoOutput(false);
}
urlConnection.connect();
return urlConnection;
}
catch (Exception e) {
if (!(e instanceof SSLHandshakeException)) {
Log.e(LOG_TAG, "", e);
}
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return null;
}
/**
* SslCertificate class does not has a public getter for the underlying
* X509Certificate, we can only do this by hack. This only works for Android 4.0+
* https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg
*/
public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
X509Certificate x509Certificate = null;
Bundle bundle = SslCertificate.saveState(sslCert);
byte[] bytes = bundle.getByteArray("x509-certificate");
if (bytes == null) {
x509Certificate = null;
} else {
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
x509Certificate = (X509Certificate) cert;
} catch (CertificateException e) {
x509Certificate = null;
}
}
return x509Certificate;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String JSONStringify(@Nullable Object value) {
if (value == null) {
return "null";
}
if (value instanceof Map) {
return new JSONObject((Map<String, Object>) value).toString();
} else if (value instanceof List) {
return new JSONArray((List<Object>) value).toString();
} else if (value instanceof String) {
return JSONObject.quote((String) value);
} else {
return JSONObject.wrap(value).toString();
}
}
public static boolean objEquals(@Nullable Object a, @Nullable Object b) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return Objects.equals(a, b);
}
return (a == b) || (a != null && a.equals(b));
}
public static String replaceAll(String s, String oldString, String newString) {
return TextUtils.join(newString, s.split(Pattern.quote(oldString)));
}
public static void log(String tag, String message) {
// Split by line, then ensure each line can fit into Log's maximum length.
for (int i = 0, length = message.length(); i < length; i++) {
int newline = message.indexOf('\n', i);
newline = newline != -1 ? newline : length;
do {
int end = Math.min(newline, i + 4000);
Log.d(tag, message.substring(i, end));
i = end;
} while (i < newline);
}
}
public static float getPixelDensity(Context context) {
return context.getResources().getDisplayMetrics().density;
}
public static Size2D getFullscreenSize(Context context) {
Size2D fullscreenSize = new Size2D(-1, -1);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wm != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
final WindowMetrics metrics = wm.getCurrentWindowMetrics();
// Gets all excluding insets
final WindowInsets windowInsets = metrics.getWindowInsets();
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
| WindowInsets.Type.displayCutout());
int insetsWidth = insets.right + insets.left;
int insetsHeight = insets.top + insets.bottom;
final Rect bounds = metrics.getBounds();
fullscreenSize.setWidth(bounds.width() - insetsWidth);
fullscreenSize.setHeight(bounds.height() - insetsHeight);
} else {
DisplayMetrics displayMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(displayMetrics);
fullscreenSize.setWidth(displayMetrics.widthPixels);
fullscreenSize.setHeight(displayMetrics.heightPixels);
}
}
return fullscreenSize;
}
public static boolean isClass(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
public static boolean isIPv6(String address) {
try {
Inet6Address.getByName(address);
} catch (UnknownHostException e) {
return false;
}
return true;
}
public static String normalizeIPv6(String address) throws Exception {
if (!Util.isIPv6(address)) {
throw new Exception("Invalid address: " + address);
}
return InetAddress.getByName(address).getCanonicalHostName();
}
public static <T> T getOrDefault(Map<String, Object> map, String key, T defaultValue) {
return map.containsKey(key) ? (T) map.get(key) : defaultValue;
}
@Nullable
public static byte[] readAllBytes(@Nullable InputStream inputStream) {
if (inputStream == null) {
return null;
}
final int bufLen = 4 * 0x400; // 4KB
byte[] buf = new byte[bufLen];
int readLen;
IOException exception = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = null;
try {
while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)
outputStream.write(buf, 0, readLen);
data = outputStream.toByteArray();
} catch (IOException e) {
exception = e;
} finally {
try {
inputStream.close();
} catch (IOException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
exception.addSuppressed(e);
}
}
try {
outputStream.close();
} catch (IOException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
exception.addSuppressed(e);
}
}
}
return data;
}
@Nullable
public static <O> Object invokeMethodIfExists(final O o, final String methodName, Object... args) {
Method[] methods = o.getClass().getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
try {
return method.invoke(o, args);
} catch (IllegalAccessException e) {
return null;
} catch (InvocationTargetException e) {
return null;
}
}
}
return null;
}
public static Drawable drawableFromBytes(Context context, byte[] data) {
return new BitmapDrawable(context.getResources(), BitmapFactory.decodeByteArray(data, 0, data.length));
}
}

View File

@ -0,0 +1,47 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class WebViewFeatureManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "WebViewFeatureManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webviewfeature";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public WebViewFeatureManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "isFeatureSupported":
String feature = (String) call.argument("feature");
result.success(WebViewFeature.isFeatureSupported(feature));
break;
case "isStartupFeatureSupported":
if (plugin != null && plugin.activity != null) {
String startupFeature = (String) call.argument("startupFeature");
result.success(WebViewFeature.isStartupFeatureSupported(plugin.activity, startupFeature));
}
break;
default:
result.notImplemented();
}
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}

View File

@ -0,0 +1,47 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.browser.customtabs.CustomTabsIntent;
public class ActionBroadcastReceiver extends BroadcastReceiver {
protected static final String LOG_TAG = "ActionBroadcastReceiver";
public static final String KEY_ACTION_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_ID";
public static final String KEY_ACTION_VIEW_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_VIEW_ID";
public static final String KEY_ACTION_MANAGER_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_MANAGER_ID";
public static final String KEY_URL_TITLE = "android.intent.extra.SUBJECT";
@Override
public void onReceive(Context context, Intent intent) {
int clickedId = intent.getIntExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_ID, -1);
String url = intent.getDataString();
if (url != null) {
Bundle b = intent.getExtras();
String viewId = b.getString(KEY_ACTION_VIEW_ID);
String managerId = b.getString(KEY_ACTION_MANAGER_ID);
if (managerId != null) {
ChromeSafariBrowserManager chromeSafariBrowserManager = ChromeSafariBrowserManager.shared.get(managerId);
if (chromeSafariBrowserManager != null) {
if (clickedId == -1) {
int id = b.getInt(KEY_ACTION_ID);
String title = b.getString(KEY_URL_TITLE);
ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId);
if (browser != null && browser.channelDelegate != null) {
browser.channelDelegate.onItemActionPerform(id, url, title);
}
} else {
ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId);
if (browser != null && browser.channelDelegate != null) {
browser.channelDelegate.onSecondaryItemActionPerform(browser.getResources().getResourceName(clickedId), url);
}
}
}
}
}
}
}

View File

@ -0,0 +1,462 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.EngagementSignalsCallback;
import com.pichillilorenzo.flutter_inappwebview_android.R;
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsActionButton;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsMenuItem;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class ChromeCustomTabsActivity extends Activity implements Disposable {
protected static final String LOG_TAG = "CustomTabsActivity";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_";
public String id;
@Nullable
public CustomTabsIntent.Builder builder;
public ChromeCustomTabsSettings customSettings = new ChromeCustomTabsSettings();
public CustomTabActivityHelper customTabActivityHelper = new CustomTabActivityHelper();
@Nullable
public CustomTabsSession customTabsSession;
public final static int CHROME_CUSTOM_TAB_REQUEST_CODE = 100;
public final static int NO_HISTORY_CHROME_CUSTOM_TAB_REQUEST_CODE = 101;
protected boolean onOpened = false;
protected boolean onCompletedInitialLoad = false;
protected boolean isBindSuccess = false;
@Nullable
public ChromeSafariBrowserManager manager;
@Nullable
public String initialUrl;
@Nullable
public List<String> initialOtherLikelyURLs;
@Nullable
public Map<String, String> initialHeaders;
@Nullable
public String initialReferrer;
public List<CustomTabsMenuItem> menuItems = new ArrayList<>();
@Nullable
public CustomTabsActionButton actionButton;
@Nullable
public CustomTabsSecondaryToolbar secondaryToolbar;
@Nullable
public ChromeCustomTabsChannelDelegate channelDelegate;
@CallSuper
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chrome_custom_tabs_layout);
Bundle b = getIntent().getExtras();
if (b == null) return;
id = b.getString("id");
String managerId = b.getString("managerId");
manager = ChromeSafariBrowserManager.shared.get(managerId);
if (manager == null || manager.plugin == null || manager.plugin.messenger == null) return;
manager.browsers.put(id, this);
MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
channelDelegate = new ChromeCustomTabsChannelDelegate(this, channel);
initialUrl = b.getString("url");
initialHeaders = (Map<String, String>) b.getSerializable("headers");
initialReferrer = b.getString("referrer");
initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs");
customSettings = new ChromeCustomTabsSettings();
customSettings.parse((HashMap<String, Object>) b.getSerializable("settings"));
actionButton = CustomTabsActionButton.fromMap((Map<String, Object>) b.getSerializable("actionButton"));
secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) b.getSerializable("secondaryToolbar"));
List<Map<String, Object>> menuItemList = (List<Map<String, Object>>) b.getSerializable("menuItemList");
for (Map<String, Object> menuItem : menuItemList) {
menuItems.add(CustomTabsMenuItem.fromMap(menuItem));
}
if (customSettings.noHistory && manager.plugin.noHistoryCustomTabsActivityCallbacks != null) {
manager.plugin.noHistoryCustomTabsActivityCallbacks.noHistoryBrowserIDs.put(id, id);
}
final ChromeCustomTabsActivity chromeCustomTabsActivity = this;
customTabActivityHelper.setConnectionCallback(new CustomTabActivityHelper.ConnectionCallback() {
@Override
public void onCustomTabsConnected() {
customTabsConnected();
if (channelDelegate != null) {
channelDelegate.onServiceConnected();
}
}
@Override
public void onCustomTabsDisconnected() {
chromeCustomTabsActivity.close();
dispose();
}
});
customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() {
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
if (navigationEvent == TAB_SHOWN && !onOpened) {
onOpened = true;
if (channelDelegate != null) {
channelDelegate.onOpened();
}
}
if (navigationEvent == NAVIGATION_FINISHED && !onCompletedInitialLoad) {
onCompletedInitialLoad = true;
if (channelDelegate != null) {
channelDelegate.onCompletedInitialLoad();
}
}
if (channelDelegate != null) {
channelDelegate.onNavigationEvent(navigationEvent);
}
}
@Override
public void extraCallback(@NonNull String callbackName, Bundle args) {
}
@Override
public void onMessageChannelReady(Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onMessageChannelReady();
}
}
@Override
public void onPostMessage(@NonNull String message, Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onPostMessage(message);
}
}
@Override
public void onRelationshipValidationResult(@CustomTabsService.Relation int relation,
@NonNull Uri requestedOrigin,
boolean result, Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onRelationshipValidationResult(relation, requestedOrigin, result);
}
}
});
}
public void launchUrl(@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
launchUrlWithSession(customTabsSession, url, headers, referrer, otherLikelyURLs);
}
public void launchUrlWithSession(@Nullable CustomTabsSession session,
@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
mayLaunchUrl(url, otherLikelyURLs);
builder = new CustomTabsIntent.Builder(session);
prepareCustomTabs();
CustomTabsIntent customTabsIntent = builder.build();
prepareCustomTabsIntent(customTabsIntent);
CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers,
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
}
public boolean mayLaunchUrl(@Nullable String url, @Nullable List<String> otherLikelyURLs) {
Uri uri = url != null ? Uri.parse(url) : null;
List<Bundle> bundleOtherLikelyURLs = new ArrayList<>();
if (otherLikelyURLs != null) {
Bundle bundleOtherLikelyURL = new Bundle();
for (String otherLikelyURL : otherLikelyURLs) {
bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL);
}
}
return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs);
}
@CallSuper
public void customTabsConnected() {
customTabsSession = customTabActivityHelper.getSession();
if (customTabsSession != null) {
try {
Bundle bundle = new Bundle();
if (customTabsSession.isEngagementSignalsApiAvailable(bundle)) {
customTabsSession.setEngagementSignalsCallback(new EngagementSignalsCallback() {
@Override
public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onVerticalScrollEvent(isDirectionUp);
}
}
@Override
public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onGreatestScrollPercentageIncreased(scrollPercentage);
}
}
@Override
public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onSessionEnded(didUserInteract);
}
}
}, bundle);
}
} catch (Throwable e) {
Log.d(LOG_TAG, "Custom Tabs Engagement Signals API not supported", e);
}
}
// avoid webpage reopen if isBindSuccess is false: onServiceConnected->launchUrl
if (isBindSuccess && initialUrl != null) {
launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
}
}
private void prepareCustomTabs() {
if (builder == null) {
return;
}
if (customSettings.addDefaultShareMenuItem != null) {
builder.setShareState(customSettings.addDefaultShareMenuItem ?
CustomTabsIntent.SHARE_STATE_ON : CustomTabsIntent.SHARE_STATE_OFF);
} else {
builder.setShareState(customSettings.shareState);
}
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
}
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
}
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
}
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
}
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
builder.setShowTitle(customSettings.showTitle);
builder.setUrlBarHidingEnabled(customSettings.enableUrlBarHiding);
builder.setInstantAppsEnabled(customSettings.instantAppsEnabled);
if (customSettings.startAnimations.size() == 2) {
builder.setStartAnimations(this,
customSettings.startAnimations.get(0).getIdentifier(this),
customSettings.startAnimations.get(1).getIdentifier(this));
}
if (customSettings.exitAnimations.size() == 2) {
builder.setExitAnimations(this,
customSettings.exitAnimations.get(0).getIdentifier(this),
customSettings.exitAnimations.get(1).getIdentifier(this));
}
for (CustomTabsMenuItem menuItem : menuItems) {
builder.addMenuItem(menuItem.getLabel(),
createPendingIntent(menuItem.getId()));
}
if (actionButton != null) {
byte[] data = actionButton.getIcon();
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(
data, 0, data.length, bitmapOptions
);
builder.setActionButton(bmp, actionButton.getDescription(),
createPendingIntent(actionButton.getId()),
actionButton.isShouldTint());
}
if (secondaryToolbar != null) {
AndroidResource layout = secondaryToolbar.getLayout();
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
clickableIDs[i] = clickableID.getIdentifier(this);
}
builder.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
}
}
public PendingIntent getSecondaryToolbarOnClickPendingIntent() {
Intent broadcastIntent = new Intent(this, ActionBroadcastReceiver.class);
Bundle extras = new Bundle();
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
broadcastIntent.putExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return PendingIntent.getBroadcast(
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
return PendingIntent.getBroadcast(
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) {
if (customSettings.packageName != null)
customTabsIntent.intent.setPackage(customSettings.packageName);
else
customTabsIntent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
if (customSettings.keepAliveEnabled)
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent);
if (customSettings.alwaysUseBrowserUI)
CustomTabsIntent.setAlwaysUseBrowserUI(customTabsIntent.intent);
}
public void updateActionButton(@NonNull byte[] icon, @NonNull String description) {
if (customTabsSession == null || actionButton == null) {
return;
}
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(
icon, 0, icon.length, bitmapOptions
);
customTabsSession.setActionButton(bmp, description);
actionButton.setIcon(icon);
actionButton.setDescription(description);
}
public void updateSecondaryToolbar(CustomTabsSecondaryToolbar secondaryToolbar) {
if (customTabsSession == null) {
return;
}
AndroidResource layout = secondaryToolbar.getLayout();
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
clickableIDs[i] = clickableID.getIdentifier(this);
}
customTabsSession.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
this.secondaryToolbar = secondaryToolbar;
}
@Override
protected void onStart() {
super.onStart();
isBindSuccess = customTabActivityHelper.bindCustomTabsService(this);
if (!isBindSuccess && initialUrl != null) {
// chrome process not running, start tab directly
launchUrlWithSession(null, initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
}
}
@Override
protected void onStop() {
super.onStop();
customTabActivityHelper.unbindCustomTabsService(this);
isBindSuccess = false;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CHROME_CUSTOM_TAB_REQUEST_CODE) {
close();
dispose();
}
}
private PendingIntent createPendingIntent(int actionSourceId) {
Intent actionIntent = new Intent(this, ActionBroadcastReceiver.class);
Bundle extras = new Bundle();
extras.putInt(ActionBroadcastReceiver.KEY_ACTION_ID, actionSourceId);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
actionIntent.putExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return PendingIntent.getBroadcast(
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
return PendingIntent.getBroadcast(
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
@Override
public void dispose() {
onStop();
onDestroy();
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
if (manager != null) {
if (manager.browsers.containsKey(id)) {
manager.browsers.put(id, null);
}
}
manager = null;
}
public void close() {
onStop();
onDestroy();
customTabsSession = null;
finish();
if (channelDelegate != null) {
channelDelegate.onClosed();
}
}
}

View File

@ -0,0 +1,7 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
public class ChromeCustomTabsActivitySingleInstance extends ChromeCustomTabsActivity {
protected static final String LOG_TAG = "ChromeCustomTabsActivitySingleInstance";
}

View File

@ -0,0 +1,251 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsService;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl {
@Nullable
private ChromeCustomTabsActivity chromeCustomTabsActivity;
public ChromeCustomTabsChannelDelegate(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity, @NonNull MethodChannel channel) {
super(channel);
this.chromeCustomTabsActivity = chromeCustomTabsActivity;
}
@Override
public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "launchUrl":
if (chromeCustomTabsActivity != null) {
String url = (String) call.argument("url");
if (url != null) {
Map<String, String> headers = (Map<String, String>) call.argument("headers");
String referrer = (String) call.argument("referrer");
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
chromeCustomTabsActivity.launchUrl(url, headers, referrer, otherLikelyURLs);
result.success(true);
} else {
result.success(false);
}
} else {
result.success(false);
}
break;
case "mayLaunchUrl":
if (chromeCustomTabsActivity != null) {
String url = (String) call.argument("url");
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
result.success(chromeCustomTabsActivity.mayLaunchUrl(url, otherLikelyURLs));
} else {
result.success(false);
}
break;
case "updateActionButton":
if (chromeCustomTabsActivity != null) {
byte[] icon = (byte[]) call.argument("icon");
String description = (String) call.argument("description");
chromeCustomTabsActivity.updateActionButton(icon, description);
result.success(true);
} else {
result.success(false);
}
break;
case "validateRelationship":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
Integer relation = (Integer) call.argument("relation");
String origin = (String) call.argument("origin");
result.success(chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null));
} else {
result.success(false);
}
break;
case "updateSecondaryToolbar":
if (chromeCustomTabsActivity != null) {
CustomTabsSecondaryToolbar secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) call.argument("secondaryToolbar"));
chromeCustomTabsActivity.updateSecondaryToolbar(secondaryToolbar);
result.success(true);
} else {
result.success(false);
}
break;
case "requestPostMessageChannel":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
String sourceOrigin = (String) call.argument("sourceOrigin");
String targetOrigin = (String) call.argument("targetOrigin");
result.success(chromeCustomTabsActivity.customTabsSession.requestPostMessageChannel(Uri.parse(sourceOrigin),
targetOrigin != null ? Uri.parse(targetOrigin) : null, new Bundle()));
} else {
result.success(false);
}
break;
case "postMessage":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
String message = (String) call.argument("message");
result.success(chromeCustomTabsActivity.customTabsSession.postMessage(message, new Bundle()));
} else {
result.success(CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR);
}
break;
case "isEngagementSignalsApiAvailable":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
try {
result.success(chromeCustomTabsActivity.customTabsSession.isEngagementSignalsApiAvailable(new Bundle()));
} catch (Throwable e) {
result.success(false);
}
} else {
result.success(false);
}
break;
case "close":
if (chromeCustomTabsActivity != null) {
chromeCustomTabsActivity.onStop();
chromeCustomTabsActivity.onDestroy();
chromeCustomTabsActivity.close();
if (chromeCustomTabsActivity.manager != null && chromeCustomTabsActivity.manager.plugin != null &&
chromeCustomTabsActivity.manager.plugin.activity != null) {
Activity activity = chromeCustomTabsActivity.manager.plugin.activity;
// https://stackoverflow.com/a/41596629/4637638
Intent myIntent = new Intent(activity, activity.getClass());
myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
myIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
activity.startActivity(myIntent);
}
chromeCustomTabsActivity.dispose();
result.success(true);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
public void onServiceConnected() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onServiceConnected", obj);
}
public void onOpened() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onOpened", obj);
}
public void onCompletedInitialLoad() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onCompletedInitialLoad", obj);
}
public void onNavigationEvent(int navigationEvent) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("navigationEvent", navigationEvent);
channel.invokeMethod("onNavigationEvent", obj);
}
public void onClosed() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onClosed", obj);
}
public void onItemActionPerform(int id, String url, String title) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("id", id);
obj.put("url", url);
obj.put("title", title);
channel.invokeMethod("onItemActionPerform", obj);
}
public void onSecondaryItemActionPerform(String name, String url) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("name", name);
obj.put("url", url);
channel.invokeMethod("onSecondaryItemActionPerform", obj);
}
public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("relation", relation);
obj.put("requestedOrigin", requestedOrigin.toString());
obj.put("result", result);
channel.invokeMethod("onRelationshipValidationResult", obj);
}
public void onMessageChannelReady() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onMessageChannelReady", obj);
}
public void onPostMessage(@NonNull String message) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("message", message);
channel.invokeMethod("onPostMessage", obj);
}
public void onVerticalScrollEvent(boolean isDirectionUp) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("isDirectionUp", isDirectionUp);
channel.invokeMethod("onVerticalScrollEvent", obj);
}
public void onGreatestScrollPercentageIncreased(int scrollPercentage) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("scrollPercentage", scrollPercentage);
channel.invokeMethod("onGreatestScrollPercentageIncreased", obj);
}
public void onSessionEnded(boolean didUserInteract) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("didUserInteract", didUserInteract);
channel.invokeMethod("onSessionEnded", obj);
}
@Override
public void dispose() {
super.dispose();
chromeCustomTabsActivity = null;
}
}

View File

@ -0,0 +1,187 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.trusted.ScreenOrientation;
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ChromeCustomTabsSettings implements ISettings<ChromeCustomTabsActivity> {
final static String LOG_TAG = "ChromeCustomTabsSettings";
@Deprecated
public Boolean addDefaultShareMenuItem;
public Integer shareState = CustomTabsIntent.SHARE_STATE_DEFAULT;
public Boolean showTitle = true;
@Nullable
public String toolbarBackgroundColor;
@Nullable
public String navigationBarColor;
@Nullable
public String navigationBarDividerColor;
@Nullable
public String secondaryToolbarColor;
public Boolean enableUrlBarHiding = false;
public Boolean instantAppsEnabled = false;
public String packageName;
public Boolean keepAliveEnabled = false;
public Boolean isSingleInstance = false;
public Boolean noHistory = false;
public Boolean isTrustedWebActivity = false;
public List<String> additionalTrustedOrigins = new ArrayList<>();
public TrustedWebActivityDisplayMode displayMode = null;
public Integer screenOrientation = ScreenOrientation.DEFAULT;
public List<AndroidResource> startAnimations = new ArrayList<>();
public List<AndroidResource> exitAnimations = new ArrayList<>();
public Boolean alwaysUseBrowserUI = false;
@NonNull
@Override
public ChromeCustomTabsSettings parse(@NonNull Map<String, Object> options) {
for (Map.Entry<String, Object> pair : options.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "addDefaultShareMenuItem":
addDefaultShareMenuItem = (Boolean) value;
break;
case "shareState":
shareState = (Integer) value;
break;
case "showTitle":
showTitle = (Boolean) value;
break;
case "toolbarBackgroundColor":
toolbarBackgroundColor = (String) value;
break;
case "navigationBarColor":
navigationBarColor = (String) value;
break;
case "navigationBarDividerColor":
navigationBarDividerColor = (String) value;
break;
case "secondaryToolbarColor":
secondaryToolbarColor = (String) value;
break;
case "enableUrlBarHiding":
enableUrlBarHiding = (Boolean) value;
break;
case "instantAppsEnabled":
instantAppsEnabled = (Boolean) value;
break;
case "packageName":
packageName = (String) value;
break;
case "keepAliveEnabled":
keepAliveEnabled = (Boolean) value;
break;
case "isSingleInstance":
isSingleInstance = (Boolean) value;
break;
case "noHistory":
noHistory = (Boolean) value;
break;
case "isTrustedWebActivity":
isTrustedWebActivity = (Boolean) value;
break;
case "additionalTrustedOrigins":
additionalTrustedOrigins = (List<String>) value;
break;
case "displayMode":
Map<String, Object> displayModeMap = (Map<String, Object>) value;
String displayModeType = (String) displayModeMap.get("type");
if (displayModeType != null) {
switch (displayModeType) {
case "IMMERSIVE_MODE":
boolean isSticky = (boolean) displayModeMap.get("isSticky");
int layoutInDisplayCutoutMode = (int) displayModeMap.get("displayCutoutMode");
displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode);
break;
case "DEFAULT_MODE":
displayMode = new TrustedWebActivityDisplayMode.DefaultMode();
break;
}
}
break;
case "screenOrientation":
screenOrientation = (Integer) value;
break;
case "startAnimations":
List<Map<String, Object>> startAnimationsList = (List<Map<String, Object>>) value;
for (Map<String, Object> startAnimation : startAnimationsList) {
AndroidResource androidResource = AndroidResource.fromMap(startAnimation);
if (androidResource != null) {
startAnimations.add(AndroidResource.fromMap(startAnimation));
}
}
break;
case "exitAnimations":
List<Map<String, Object>> exitAnimationsList = (List<Map<String, Object>>) value;
for (Map<String, Object> exitAnimation : exitAnimationsList) {
AndroidResource androidResource = AndroidResource.fromMap(exitAnimation);
if (androidResource != null) {
exitAnimations.add(AndroidResource.fromMap(exitAnimation));
}
}
break;
case "alwaysUseBrowserUI":
alwaysUseBrowserUI = (Boolean) value;
break;
}
}
return this;
}
@NonNull
@Override
public Map<String, Object> toMap() {
Map<String, Object> options = new HashMap<>();
options.put("addDefaultShareMenuItem", addDefaultShareMenuItem);
options.put("showTitle", showTitle);
options.put("toolbarBackgroundColor", toolbarBackgroundColor);
options.put("navigationBarColor", navigationBarColor);
options.put("navigationBarDividerColor", navigationBarDividerColor);
options.put("secondaryToolbarColor", secondaryToolbarColor);
options.put("enableUrlBarHiding", enableUrlBarHiding);
options.put("instantAppsEnabled", instantAppsEnabled);
options.put("packageName", packageName);
options.put("keepAliveEnabled", keepAliveEnabled);
options.put("isSingleInstance", isSingleInstance);
options.put("noHistory", noHistory);
options.put("isTrustedWebActivity", isTrustedWebActivity);
options.put("additionalTrustedOrigins", additionalTrustedOrigins);
options.put("screenOrientation", screenOrientation);
options.put("alwaysUseBrowserUI", alwaysUseBrowserUI);
return options;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity) {
Map<String, Object> realOptions = toMap();
if (chromeCustomTabsActivity != null) {
Intent intent = chromeCustomTabsActivity.getIntent();
if (intent != null) {
realOptions.put("packageName", intent.getPackage());
realOptions.put("keepAliveEnabled", intent.hasExtra(CustomTabsHelper.EXTRA_CUSTOM_TABS_KEEP_ALIVE));
}
}
return realOptions;
}
}

View File

@ -0,0 +1,139 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.Util;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ChromeSafariBrowserManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "ChromeBrowserManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public String id;
public static final Map<String, ChromeSafariBrowserManager> shared = new HashMap<>();
public final Map<String, ChromeCustomTabsActivity> browsers = new HashMap<>();
public ChromeSafariBrowserManager(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.id = UUID.randomUUID().toString();
this.plugin = plugin;
shared.put(this.id, this);
}
@Override
public void onMethodCall(final MethodCall call, final MethodChannel.Result result) {
final String viewId = (String) call.argument("id");
switch (call.method) {
case "open":
if (plugin != null && plugin.activity != null) {
String url = (String) call.argument("url");
HashMap<String, Object> headers = (HashMap<String, Object>) call.argument("headers");
String referrer = (String) call.argument("referrer");
ArrayList<String> otherLikelyURLs = (ArrayList<String>) call.argument("otherLikelyURLs");
HashMap<String, Object> settings = (HashMap<String, Object>) call.argument("settings");
HashMap<String, Object> actionButton = (HashMap<String, Object>) call.argument("actionButton");
HashMap<String, Object> secondaryToolbar = (HashMap<String, Object>) call.argument("secondaryToolbar");
List<HashMap<String, Object>> menuItemList = (List<HashMap<String, Object>>) call.argument("menuItemList");
open(plugin.activity, viewId, url, headers, referrer, otherLikelyURLs, settings, actionButton, secondaryToolbar, menuItemList, result);
} else {
result.success(false);
}
break;
case "isAvailable":
if (plugin != null && plugin.activity != null) {
result.success(CustomTabActivityHelper.isAvailable(plugin.activity));
} else {
result.success(false);
}
break;
case "getMaxToolbarItems":
result.success(CustomTabsIntent.getMaxToolbarItems());
break;
case "getPackageName":
if (plugin != null && plugin.activity != null) {
ArrayList<String> packages = (ArrayList<String>) call.argument("packages");
Boolean ignoreDefault = (Boolean) call.argument("ignoreDefault");
result.success(CustomTabsClient.getPackageName(plugin.activity, packages, ignoreDefault));
} else {
result.success(null);
}
break;
default:
result.notImplemented();
}
}
public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap<String, Object> headers,
@Nullable String referrer, @Nullable ArrayList<String> otherLikelyURLs,
HashMap<String, Object> settings, HashMap<String, Object> actionButton,
HashMap<String, Object> secondaryToolbar,
List<HashMap<String, Object>> menuItemList, MethodChannel.Result result) {
Intent intent = null;
Bundle extras = new Bundle();
extras.putString("url", url);
extras.putString("id", viewId);
extras.putString("managerId", this.id);
extras.putSerializable("headers", headers);
extras.putString("referrer", referrer);
extras.putSerializable("otherLikelyURLs", otherLikelyURLs);
extras.putSerializable("settings", settings);
extras.putSerializable("actionButton", (Serializable) actionButton);
extras.putSerializable("secondaryToolbar", (Serializable) secondaryToolbar);
extras.putSerializable("menuItemList", (Serializable) menuItemList);
Boolean isSingleInstance = Util.<Boolean>getOrDefault(settings, "isSingleInstance", false);
Boolean isTrustedWebActivity = Util.<Boolean>getOrDefault(settings, "isTrustedWebActivity", false);
if (CustomTabActivityHelper.isAvailable(activity)) {
intent = new Intent(activity, !isSingleInstance ?
(!isTrustedWebActivity ? ChromeCustomTabsActivity.class : TrustedWebActivity.class) :
(!isTrustedWebActivity ? ChromeCustomTabsActivitySingleInstance.class : TrustedWebActivitySingleInstance.class));
intent.putExtras(extras);
Boolean noHistory = Util.<Boolean>getOrDefault(settings, "noHistory", false);
if (noHistory) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
}
activity.startActivity(intent);
result.success(true);
return;
}
result.error(LOG_TAG, "ChromeCustomTabs is not available!", null);
}
@Override
public void dispose() {
super.dispose();
Collection<ChromeCustomTabsActivity> browserList = browsers.values();
for (ChromeCustomTabsActivity browser : browserList) {
if (browser != null) {
browser.close();
browser.dispose();
}
}
browsers.clear();
shared.remove(this.id);
plugin = null;
}
}

View File

@ -0,0 +1,177 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Browser;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.trusted.TrustedWebActivityIntent;
import java.util.List;
import java.util.Map;
/**
* This is a helper class to manage the connection to the Custom Tabs Service.
*/
public class CustomTabActivityHelper implements ServiceConnectionCallback {
private CustomTabsSession mCustomTabsSession;
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;
private ConnectionCallback mConnectionCallback;
private CustomTabsCallback mCustomTabsCallback;
/**
* Opens the URL on a Custom Tab if possible.
*
* @param activity The host activity.
* @param intent a intent to be used if Custom Tabs is available.
* @param uri the Uri to be opened.
*/
public static void openCustomTab(Activity activity,
Intent intent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
intent.setData(uri);
if (headers != null) {
Bundle bundleHeaders = new Bundle();
for (Map.Entry<String, String> header : headers.entrySet()) {
bundleHeaders.putString(header.getKey(), header.getValue());
}
intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders);
}
if (referrer != null) {
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
}
activity.startActivityForResult(intent, requestCode);
}
public static void openCustomTab(Activity activity,
CustomTabsIntent customTabsIntent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
CustomTabActivityHelper.openCustomTab(activity, customTabsIntent.intent, uri,
headers, referrer, requestCode);
}
public static void openTrustedWebActivity(Activity activity,
TrustedWebActivityIntent trustedWebActivityIntent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
CustomTabActivityHelper.openCustomTab(activity, trustedWebActivityIntent.getIntent(), uri,
headers, referrer, requestCode);
}
public static boolean isAvailable(Activity activity) {
return CustomTabsHelper.getPackageNameToUse(activity) != null;
}
/**
* Unbinds the Activity from the Custom Tabs Service.
* @param activity the activity that is connected to the service.
*/
public void unbindCustomTabsService(Activity activity) {
if (mConnection == null) return;
activity.unbindService(mConnection);
mClient = null;
mCustomTabsSession = null;
mConnection = null;
}
/**
* Creates or retrieves an exiting CustomTabsSession.
*
* @return a CustomTabsSession.
*/
@Nullable
public CustomTabsSession getSession() {
if (mClient == null) {
mCustomTabsSession = null;
} else if (mCustomTabsSession == null) {
mCustomTabsSession = mClient.newSession(mCustomTabsCallback);
}
return mCustomTabsSession;
}
/**
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
* @param connectionCallback
*/
public void setConnectionCallback(ConnectionCallback connectionCallback) {
this.mConnectionCallback = connectionCallback;
}
public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) {
this.mCustomTabsCallback = customTabsCallback;
}
/**
* Binds the Activity to the Custom Tabs Service.
* @param activity the activity to be binded to the service.
*/
public boolean bindCustomTabsService(Activity activity) {
if (mClient != null) return true;
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
if (packageName == null) return false;
mConnection = new ServiceConnection(this);
return CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
}
/**
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
* @return true if call to mayLaunchUrl was accepted.
*/
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
if (mClient == null) return false;
CustomTabsSession session = getSession();
if (session == null) return false;
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
}
@Override
public void onServiceConnected(CustomTabsClient client) {
mClient = client;
mClient.warmup(0L);
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
}
@Override
public void onServiceDisconnected() {
mClient = null;
mCustomTabsSession = null;
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
}
/**
* A Callback for when the service is connected or disconnected. Use those callbacks to
* handle UI changes when the service is connected or disconnected.
*/
public interface ConnectionCallback {
/**
* Called when the service is connected.
*/
void onCustomTabsConnected();
/**
* Called when the service is disconnected.
*/
void onCustomTabsDisconnected();
}
}

View File

@ -0,0 +1,134 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.browser.customtabs.CustomTabsService;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class for Custom Tabs.
*/
public class CustomTabsHelper {
protected static final String TAG = "CustomTabsHelper";
static final String STABLE_PACKAGE = "com.android.chrome";
static final String BETA_PACKAGE = "com.chrome.beta";
static final String DEV_PACKAGE = "com.chrome.dev";
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
protected static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse;
private CustomTabsHelper() {}
public static void addKeepAliveExtra(Context context, Intent intent) {
Intent keepAliveIntent = new Intent().setClassName(
context.getPackageName(), KeepAliveService.class.getCanonicalName());
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
}
/**
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
* the one chosen by the user if there is one, otherwise makes a best effort to return a
* valid package name.
*
* This is <strong>not</strong> threadsafe.
*
* @param context {@link Context} to use for accessing {@link PackageManager}.
* @return The package name recommended to use for connecting to custom tabs related components.
*/
public static String getPackageNameToUse(Context context) {
if (sPackageNameToUse != null) return sPackageNameToUse;
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
activityIntent.addCategory(Intent.CATEGORY_BROWSABLE);
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
String defaultViewHandlerPackageName = null;
if (defaultViewHandlerInfo != null) {
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
}
// Get all apps that can handle VIEW intents.
int flags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PackageManager.MATCH_ALL;
}
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, flags);
List<String> packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);
if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
}
}
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
// and service calls.
if (packagesSupportingCustomTabs.isEmpty()) {
sPackageNameToUse = null;
} else if (packagesSupportingCustomTabs.size() == 1) {
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
sPackageNameToUse = defaultViewHandlerPackageName;
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
sPackageNameToUse = STABLE_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
sPackageNameToUse = BETA_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
sPackageNameToUse = DEV_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
sPackageNameToUse = LOCAL_PACKAGE;
}
return sPackageNameToUse;
}
/**
* Used to check whether there is a specialized handler for a given intent.
* @param intent The intent to check with.
* @return Whether there is a specialized handler for the given intent.
*/
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
try {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(
intent,
PackageManager.GET_RESOLVED_FILTER);
if (handlers == null || handlers.size() == 0) {
return false;
}
for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;
if (filter == null) continue;
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
if (resolveInfo.activityInfo == null) continue;
return true;
}
} catch (RuntimeException e) {
Log.e(TAG, "Runtime exception while getting specialized handlers");
}
return false;
}
/**
* @return All possible chrome package names that provide custom tabs feature.
*/
public static String[] getPackages() {
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
}
}

View File

@ -0,0 +1,18 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
/**
* Empty service used by the custom tab to bind to, raising the application's importance.
*/
public class KeepAliveService extends Service {
private static final Binder sBinder = new Binder();
@Override
public IBinder onBind(Intent intent) {
return sBinder;
}
}

View File

@ -0,0 +1,77 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import io.flutter.embedding.android.FlutterActivity;
public class NoHistoryCustomTabsActivityCallbacks implements Disposable {
@Nullable
public InAppWebViewFlutterPlugin plugin;
public final Map<String, String> noHistoryBrowserIDs = new HashMap<>();
public NoHistoryCustomTabsActivityCallbacks(@NonNull final InAppWebViewFlutterPlugin plugin) {
this.plugin = plugin;
}
public Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activity instanceof FlutterActivity && plugin != null && plugin.chromeSafariBrowserManager != null) {
Collection<String> browserIds = noHistoryBrowserIDs.values();
for (String browserId : browserIds) {
if (browserId != null) {
noHistoryBrowserIDs.put(browserId, null);
ChromeCustomTabsActivity browser = plugin.chromeSafariBrowserManager.browsers.get(browserId);
if (browser != null) {
browser.close();
browser.dispose();
}
}
}
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
@Override
public void dispose() {
noHistoryBrowserIDs.clear();
plugin = null;
}
}

View File

@ -0,0 +1,32 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.ComponentName;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import java.lang.ref.WeakReference;
/**
* Implementation for the CustomTabsServiceConnection that avoids leaking the
* ServiceConnectionCallback
*/
public class ServiceConnection extends CustomTabsServiceConnection {
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
mConnectionCallback = new WeakReference<>(connectionCallback);
}
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
}
@Override
public void onServiceDisconnected(ComponentName name) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
}
}

View File

@ -0,0 +1,19 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import androidx.browser.customtabs.CustomTabsClient;
/**
* Callback for events when connecting and disconnecting from Custom Tabs Service.
*/
public interface ServiceConnectionCallback {
/**
* Called when the service is connected.
* @param client a CustomTabsClient
*/
void onServiceConnected(CustomTabsClient client);
/**
* Called when the service is disconnected.
*/
void onServiceDisconnected();
}

View File

@ -0,0 +1,84 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.trusted.TrustedWebActivityIntent;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import java.util.List;
import java.util.Map;
public class TrustedWebActivity extends ChromeCustomTabsActivity {
protected static final String LOG_TAG = "TrustedWebActivity";
public TrustedWebActivityIntentBuilder builder;
@Override
public void launchUrl(@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
if (customTabsSession == null) {
return;
}
Uri uri = Uri.parse(url);
mayLaunchUrl(url, otherLikelyURLs);
builder = new TrustedWebActivityIntentBuilder(uri);
prepareCustomTabs();
TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession);
prepareCustomTabsIntent(trustedWebActivityIntent);
CustomTabActivityHelper.openTrustedWebActivity(this, trustedWebActivityIntent, uri, headers,
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
}
private void prepareCustomTabs() {
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
}
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
}
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
}
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
}
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
if (customSettings.additionalTrustedOrigins != null && !customSettings.additionalTrustedOrigins.isEmpty()) {
builder.setAdditionalTrustedOrigins(customSettings.additionalTrustedOrigins);
}
if (customSettings.displayMode != null) {
builder.setDisplayMode(customSettings.displayMode);
}
builder.setScreenOrientation(customSettings.screenOrientation);
}
private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivityIntent) {
Intent intent = trustedWebActivityIntent.getIntent();
if (customSettings.packageName != null)
intent.setPackage(customSettings.packageName);
else
intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
if (customSettings.keepAliveEnabled)
CustomTabsHelper.addKeepAliveExtra(this, intent);
if (customSettings.alwaysUseBrowserUI)
CustomTabsIntent.setAlwaysUseBrowserUI(intent);
}
}

View File

@ -0,0 +1,7 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
public class TrustedWebActivitySingleInstance extends TrustedWebActivity {
protected static final String LOG_TAG = "TrustedWebActivitySingleInstance";
}

Some files were not shown because too many files have changed in this diff Show More