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
parent
2713517456
commit
bf6429dc76
|
|
@ -59,3 +59,8 @@ app.*.map.json
|
|||
|
||||
# 내장 Chrome: 빌드할 때 내려받는 CEF 배포본 (약 330MB)
|
||||
/packages/webview_cef/third/cef/
|
||||
|
||||
# Android 빌드 중간 파일
|
||||
/android/build/
|
||||
/packages/*/android/.cxx/
|
||||
/packages/*/android/build/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/**")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 화면.
|
||||
/// 트레이 · 단축키 · 다운로드 · 브라우저는 원래 창이 맡는다. 닫으면 이 창만 끝난다.
|
||||
/// 동영상 목록은 원래 창과 같은 파일을 보므로 어느 창에서 넣거나 지워도 서로 맞춰진다.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('허용했으면 다시 확인')),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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('패키지 라이선스'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 폴더 제거
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
BIN
packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
packages/ffmpeg_kit_flutter_new_min_gpl/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
|
|
@ -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" "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
rootProject.name = 'ffmpeg-kit-flutter-android'
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.antonkarpenko.ffmpegkit">
|
||||
</manifest>
|
||||
|
|
@ -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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ≥ 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 ≥ 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 ≥ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 폴더 제거
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'flutter_inappwebview_android'
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue