用到的插件
dio: ^4.0.1
ffmpeg_kit_flutter:^4.5.1//视频处理操作
image_gallery_saver: ^1.7.1//保存到相册
path_provider: ^2.0.6 //路径查询
注:用ffmpeg_kit_flutter这个的原因是因为项目用到了video_editor它引了。可以用flutter_ffmpeg,一样的。
加图片的命令:
String command = "-i " +
_videoInput +
" -i " +
_imagePath +
" -filter_complex overlay=main_w-overlay_w-20:main_h-overlay_h-20 " +
_videoOutput +
"";
_videoInput :视频路径
_imagePath :图片路径
_videoOutput :加了水印后的视频存放路径
加文字的命令:
String command = "-i " +
sss! +
" -vf " +
"drawtext=fontsize=32:fontcolor=red:text='helloWorld':alpha=0.8 " +
output! +
"";
这里有个小插曲:就是我文字添加不了,老是报没有drawtext:No such filter: 'drawtext',按网上的方法也不行,无奈放弃。那就换一种实现方式。文字和图片合在一起,生成一张新的图片,再通过加图片的命令去操作。
1、获取水印图片(文字加图片)单纯只加图片的,这一步略过
Future<Uint8List?> _getWatermark({String? personalId}) async {
var pictureRecorder = new ui.PictureRecorder();
var canvas = new Canvas(pictureRecorder);
var images = await getImage(
'assets/images/collect_live.png',//assets的图片路径
);
Paint _linePaint = new Paint();
// 绘制图片
canvas.drawImage(images, Offset(32, 0), _linePaint); // 这个Offset是值可以自己算(0,0起点开始,中间的话就是画布宽度-2*图片的宽度):图片的宽就是分辨率的宽。
// 绘制文字
ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.center,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.normal,
fontSize: 12.0));
pb.pushStyle(ui.TextStyle(color: Colors.white));
pb.addText('抖音号:1231454');
// 设置文本的宽度约束
ParagraphConstraints pc = ui.ParagraphConstraints(width: 100);
ui.Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(paragraph, Offset(0, images.height.toDouble() + 5));
var picture =
await pictureRecorder.endRecording().toImage(100, 60); //设置生成图片的宽和高
var pngImageBytes =
await picture.toByteData(format: ui.ImageByteFormat.png);
return pngImageBytes?.buffer.asUint8List();
}
//图片转换
Future<ui.Image> getImage(String asset) async {
ByteData data = await rootBundle.load(asset);
Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
2、视频加水印并保存到本地操作
FFmpegKit.executeAsync(
command,
(
Session session,
) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
final result = await ImageGallerySaver.saveFile(_videoOutput);
if (result['isSuccess'] == true) {
Fluttertoast.showToast(msg: '视频已保存到本地');
} else if (result['isSuccess'] == false) {
Fluttertoast.showToast(msg: '视频保存失败');
}
} else if (ReturnCode.isCancel(returnCode)) {
print('取消');
} else {
print('错误');
Fluttertoast.showToast(msg: '视频处理错误');
}
});
3、还有一个问题就是进度条。下载网络视频到本地的时间,和视频加水印的处理时间。一般是把这两个时间和在一起显示成一个进度。下面会实现一个简单的进度条样式。
网络视频下载进度回调:
typedef ProgressCallback = void Function(int count, int total);
视频处理的进度回调:
回调方法可以拿到时间:getTime() 单位是毫秒
完整代码:
///保存视频
_saveVideo(CommunityTopicEntityEntity items) async {
late final ValueNotifier<double> _notifier = ValueNotifier<double>(0.0);
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return UnconstrainedBox(
constrainedAxis: Axis.vertical,
child: SizedBox(
width: 120,
child: Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
alignment: Alignment.topRight,
children: [
Container(
height: 80,
color: AppColors.black_1f00,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder(
valueListenable: _notifier,
builder: (context, double value, child) {
return Stack(
alignment: Alignment.center,
children: [
Container(
height: 35,
width: 35,
child: CircularProgressIndicator(
value: value,
backgroundColor: Colors.white,
valueColor:
AlwaysStoppedAnimation<Color>(
AppColors.black_33),
)),
Text(
"${(value * 100).toStringAsFixed(0)}%",
style: TextStyle(
color: Colors.white,
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
),
],
);
},
),
SizedBox(
height: 5,
),
Text(
'正在保存至本地...',
style: TextStyle(
color: AppColors.black_33, fontSize: 12),
)
],
),
),
),
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(
Icons.close,
size: 22,
color: AppColors.black_33,
),
)
],
)),
),
);
},
);
Uint8List? pngBytes = await _getWatermark();
Directory _directory = await getTemporaryDirectory();
//临时路径
String _imagePath = _directory.path + '/image.png'; //水印图片
String? _videoInput = _directory.path + '/input.mp4'; //处理的视频
String? _videoOutput = _directory.path + '/output.mp4'; //得到的视频
File file = File(_imagePath);
file.writeAsBytes(pngBytes!);
double _count = 0;
double _total = 0;
await Dio().download(items.content!.video!, _videoInput,
onReceiveProgress: (count, total) {
if (total != -1) {
_count = count.toDouble();
_total = total + items.content!.duration! * 1000; //总时间
_notifier.value = _count / _total;
print('total:$total');
print((count / total * 100).toStringAsFixed(0) + '%');
}
});
String command = "-i " +
_videoInput +
" -i " +
_imagePath +
" -filter_complex overlay=main_w-overlay_w-20:main_h-overlay_h-20 " +
_videoOutput +
"";
FFmpegKit.executeAsync(
command,
(
Session session,
) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
final result = await ImageGallerySaver.saveFile(_videoOutput);
print(result['isSuccess']);
if (result['isSuccess'] == true) {
Navigator.pop(context);
Fluttertoast.showToast(msg: '视频已保存到本地');
} else if (result['isSuccess'] == false) {
Fluttertoast.showToast(msg: '视频保存失败');
}
} else if (ReturnCode.isCancel(returnCode)) {
print('取消');
} else {
print('错误');
Fluttertoast.showToast(msg: '视频处理错误');
}
},
null,
(statistics) {
_notifier.value = (_count + statistics.getTime().toDouble()) / _total;
});
}
///获取视频水印
Future<Uint8List?> _getWatermark({String? personalId}) async {
var pictureRecorder = new ui.PictureRecorder(); // 图片记录仪
var canvas = new Canvas(pictureRecorder); //canvas接受一个图片记录仪
var images = await getImage(
'assets/images/collect_live.png',
);
Paint _linePaint = new Paint();
// 绘制图片
canvas.drawImage(images, Offset(32, 0), _linePaint); // 直接画图
// 绘制文字
ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.center,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.normal,
fontSize: 12.0));
pb.pushStyle(ui.TextStyle(color: Colors.white));
pb.addText('喵职号:1231454');
// 设置文本的宽度约束
ParagraphConstraints pc = ui.ParagraphConstraints(width: 100);
ui.Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(paragraph, Offset(0, images.height.toDouble() + 5));
var picture =
await pictureRecorder.endRecording().toImage(100, 60); //设置生成图片的宽和高
var pngImageBytes =
await picture.toByteData(format: ui.ImageByteFormat.png);
return pngImageBytes?.buffer.asUint8List();
}
//图片转换
Future<ui.Image> getImage(String asset) async {
ByteData data = await rootBundle.load(asset);
Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
简单使用:
_saveVideo(widget.items!);
说明:代码中
items.content!.video!替换成自己的视频路径。
items.content!.duration!这个是视频的时间。如果是毫秒就就不要*1000了
Color的得自己替换。