Skip to content

Instantly share code, notes, and snippets.

@ampersanda
Last active February 6, 2024 16:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ampersanda/e8c9efaa6670ce590d6f1cb31ce05e93 to your computer and use it in GitHub Desktop.
Save ampersanda/e8c9efaa6670ce590d6f1cb31ce05e93 to your computer and use it in GitHub Desktop.
Native drag and drop video splitter
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
import 'package:macos_ui/macos_ui.dart';
import 'package:process_run/process_run.dart';
import 'package:super_clipboard/src/formats_base.dart';
import 'package:super_clipboard/src/reader.dart';
import 'package:super_drag_and_drop/super_drag_and_drop.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MacosApp(
title: 'Video Splitter',
theme: MacosThemeData.light(),
darkTheme: MacosThemeData.dark(),
debugShowCheckedModeBanner: false,
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool _splitting = false;
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) async {
final bool ffmpegInstalled = await _checkIsFfmpegInstalled();
if (!ffmpegInstalled && mounted) {
showMacosAlertDialog(
context: context,
builder: (_) {
bool installing = false;
return StatefulBuilder(
builder: (BuildContext context,
void Function(void Function()) setState) {
return MacosAlertDialog(
appIcon: const MacosIcon(
CupertinoIcons.checkmark_circle_fill,
color: CupertinoColors.inactiveGray,
size: 56.0,
),
title: Text(
'ffmpeg is required!',
style: MacosTheme.of(context).typography.headline,
),
message: const Text(
'Please install ffmpeg before continue using this app',
textAlign: TextAlign.center,
),
primaryButton: PushButton(
controlSize: ControlSize.large,
onPressed: installing
? null
: () async {
setState(() {
installing = true;
});
final bool installed = await _installFfmpeg();
if (installed && mounted) {
Navigator.pop(context);
}
setState(() {
installing = false;
});
},
child: Text(installing
? 'Installing...'
: 'Install using Homebrew'),
),
);
},
);
},
);
}
});
}
Future<bool> _installFfmpeg() async {
final Shell shell = Shell();
try {
final List<ProcessResult> results =
await shell.run('brew install ffmpeg');
return results.firstOrNull?.exitCode == 0;
} catch (e) {
return false;
}
}
Future<bool> _checkIsFfmpegInstalled() async {
final Shell shell = Shell();
try {
final List<ProcessResult> results = await shell.run('which ffmpeg');
return results.firstOrNull?.exitCode == 0;
} catch (e) {
return false;
}
}
Future<bool> _splitVideo(Uri uri) async {
final Shell shell = Shell();
final File file = File.fromUri(uri);
final String folder = file.parent.path;
final String? fileName = uri.pathSegments.lastOrNull;
final String? extension = fileName?.split('.').lastOrNull;
if (fileName == null || extension == null) {
return false;
}
try {
final Directory splitFolder =
await Directory('$folder/$fileName.splitted').create();
final List<ProcessResult> results = await shell.run(
'ffmpeg -i "${file.path}" -c copy -map 0 -segment_time 60 -f segment -reset_timestamps 1 "${splitFolder.path}/target_%d.$extension"');
return results.firstOrNull?.exitCode == 0;
} catch (e) {
return false;
} finally {
setState(() {
_splitting = false;
});
}
}
static const List<SimpleFileFormat> _acceptedFormats = <SimpleFileFormat>[
Formats.mp4,
Formats.mov,
Formats.webm
];
@override
Widget build(BuildContext context) {
return MacosScaffold(
children: <Widget>[
ContentArea(
builder: (BuildContext context, ScrollController scrollController) {
return ColoredBox(
color: CupertinoColors.darkBackgroundGray,
child: SizedBox.expand(
child: DropRegion(
formats: _acceptedFormats,
hitTestBehavior: HitTestBehavior.opaque,
onDropOver: (DropOverEvent event) {
if (_splitting) {
return DropOperation.none;
}
// You can inspect local data here, as well as formats of each item.
// However on certain platforms (mobile / web) the actual data is
// only available when the drop is accepted (onPerformDrop).
final DropItem item = event.session.items.first;
// This drop region only supports copy operation.
if (event.session.allowedOperations
.contains(DropOperation.link)) {
if (_acceptedFormats.any((SimpleFileFormat format) =>
item.canProvide(format))) {
return DropOperation.link;
}
}
return DropOperation.none;
},
onPerformDrop: (PerformDropEvent event) async {
final DropItem item = event.session.items.first;
final DataReader reader = item.dataReader!;
if (_acceptedFormats.any(
(SimpleFileFormat format) => item.canProvide(format))) {
reader.getValue(Formats.fileUri, (Uri? uri) {
if (uri == null) {
return;
}
_splitVideo(uri);
});
}
},
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Center(
child: Text(
_splitting ? 'Splitting...' : 'Drop Video Here')),
),
),
),
);
},
)
],
);
}
}
name: videosplit
description: "Split video into chunks using ffmpeg"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=3.2.5 <4.0.0'
dependencies:
flutter:
sdk: flutter # 3.16.8
cupertino_icons: ^1.0.2
super_drag_and_drop: ^0.8.4
process_run: ^0.14.1+3
local_hero: ^0.3.0
macos_ui: ^2.0.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment