flutter视频加水印(加处理进度条)

用到的插件

 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、还有一个问题就是进度条。下载网络视频到本地的时间,和视频加水印的处理时间。一般是把这两个时间和在一起显示成一个进度。下面会实现一个简单的进度条样式。
网络视频下载进度回调:


image.png

typedef ProgressCallback = void Function(int count, int total);
视频处理的进度回调:


image.png

回调方法可以拿到时间: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的得自己替换。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容