介绍

本篇文章使用了ffmpeg_kit_flutter,Image,image_picker,path_provider等依赖,主要实现了给视频,图片添加水印的功能,该添加水印不会随着视频,图片的格式,分辨率的变化而变化。

提前需要准备的方法

其中的ui和img是这样的。

import 'dart:ui' as ui;

import 'package:image/image.dart' as img;

制作水印图片的方法

该方法中的位置设置都是设置好的,位置是右下角(该是获取的手机屏幕的分辨率来调整的)

static Future<Uint8List?> _getWatermark({String? id, String? name}) async {
    var imgWidth = ui.window.physicalSize.width;
    var pictureRecorder = ui.PictureRecorder();
    var canvas = Canvas(pictureRecorder);
    var images = await getImage("" //assets的图片路径
    );
    var width = images.width;
    Paint _linePaint = Paint()..color = const Color.fromRGBO(0, 0, 0, 1);
    // 绘制图片
    canvas.drawImage(images, Offset(imgWidth - width - 21, 0),
        _linePaint); // 这个Offset是值可以自己算(0,0起点开始,中间的话就是画布宽度-2*图片的宽度):图片的宽就是分辨率的宽。
    // 绘制文字
    ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
      textAlign: TextAlign.right,
      fontWeight: FontWeight.w500,
      fontStyle: FontStyle.normal,
      fontSize: 24.0.px,
    ));
    pb.pushStyle(ui.TextStyle(color: Colors.white));
    pb.addText("$name:$id");

    ///计算数据的长度
    var textPainter = TextPainter(
      text: TextSpan(
        text: "$name:$id",
        style: TextStyle(fontSize: 24.px, fontWeight: FontWeight.w500),
      ),
      textDirection: TextDirection.ltr,
      textAlign: TextAlign.left,
      textScaleFactor: 1.0,
    );
    textPainter.layout();
    var textImgWidth = textPainter.width;
    // 设置文本的宽度约束
    var pc = ui.ParagraphConstraints(width: imgWidth - 23);
    ui.Paragraph paragraph = pb.build()..layout(pc);
    canvas.drawParagraph(paragraph, Offset(-5, images.height.toDouble() + 20));

    ///判断名称的长度
    if (imgWidth - 23 <= textImgWidth) {
      picture = await pictureRecorder
          .endRecording()
          .toImage(imgWidth.toInt() - 10, (images.height + 102)); //设置生成图片的宽和高

      var pngImageBytes =
          await picture.toByteData(format: ui.ImageByteFormat.png);
      return pngImageBytes?.buffer.asUint8List();
    } else {
      picture = await pictureRecorder
          .endRecording()
          .toImage(imgWidth.toInt() - 10, (images.height + 79)); //设置生成图片的宽和高
      var pngImageBytes =
          await picture.toByteData(format: ui.ImageByteFormat.png);
      return pngImageBytes?.buffer.asUint8List();
    }
  }

  //图片转换
  static Future<ui.Image> getImage(String asset) async {
    ByteData data = await rootBundle.load(asset);
    var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    var fi = await codec.getNextFrame();
    return fi.image;
  }

图片添加水印

给图片添加水印使用到了ffmpeg的命令,除此之外还需要获取图片的分辨率,之后保存到本地。

该代码是ffmpeg的主要命令,其中的url,_imagePath分别是网络图片的路径跟水印图片,


${appDocDir.path}/${time}.png呢是图片保存的地址。


String command = "-i " +
          url +
          " -i " +
          " $_imagePath " +
          " -filter_complex [1:v]scale=${width * imageWH}:${height * imageWH}[wm];[0:v][wm]overlay=main_w-overlay_w-10:main_h-overlay_h-10 -qscale:v 1 " +
          " ${appDocDir.path}/${time}.png";

这个是完整的图片添加水印代码:(文章中的_getCurrentTime是写的一个获取当前时间的代码)

static var picture;//这个属性是接收水印图片的大小
 static Future<String?> _saveNetworkImage(String url,
      {String? userId, String? nickName}) async {
    //userId跟nickName是我需要水印的id和名称
    if (userId != null && nickName != null) {
      //该是获取缓存临时路径
      var appDocDir = await getTemporaryDirectory();
      //该方法是使用了画笔生成的图
      Uint8List? pngBytes = await _getWatermark(id: userId, name: nickName);
      //自己创建的水印图片的路径
      String _imagePath = appDocDir.path + '/syimage.png'; //水印图片
      File file = File(_imagePath);
      //将水印图片保存到_imagePath 中
      await file.writeAsBytes(pngBytes!);
      //该方法是获取图片的分辨率
      var getNetworkImageSize = await _getNetworkImageSize(url);
      //水印图片的宽高
      var width = picture.width;
      var height = picture.height;
      //网络图片的宽高
      var imageWidth = getNetworkImageSize.width;
      var imageHeight = getNetworkImageSize.height;
      //获取最小的宽高比的值,用来调整水印的大小
      var imageWH = imageWidth / width > imageHeight / height
          ? imageHeight / height
          : imageWidth / width;

      var time = _getCurrentTime();
      ///ffmpeg命令生成图片水印
      String command = "-i " +
          url +
          " -i " +
          " $_imagePath " +
          " -filter_complex [1:v]scale=${width * imageWH}:${height * imageWH}[wm];[0:v][wm]overlay=main_w-overlay_w-10:main_h-overlay_h-10 -qscale:v 1 " +
          " ${appDocDir.path}/${time}.png";

      //执行ffmpeg命令,并继续其他操作
      await FFmpegKit.executeAsync(command, (session) async {
        final returnCode = await session.getReturnCode();
        //ffmpeg命令执行是否成功。returnCode = 0成功:1失败
        if (ReturnCode.isSuccess(returnCode)) {
          //保存到本地---自己在网上查
        } else {
          //ffmpeg命令执行失败
        }
      });
    } else {
        //如果没有传递userId和nickName直接保存
    }
  }

视频添加水印

给视频添加水印跟着图片呢是差不多的,步骤都是一样的,ffmpag命令也没有差太多,可以说是一样的,接下来上ffmpeg命令代码:

String command = "-i " +
          savePath +
          " -i " +
          " $_imagePath " +
          "  -filter_complex [1:v]scale=${width * imageWH}:${height * imageWH}[wm];[0:v][wm]overlay=main_w-overlay_w-10:main_h-overlay_h-10 " +
          //这一行好像是提高视频的质量的,不是特别清楚,自己可以看看
          "  -acodec copy  -q:v 0  -q:a 0 " +
          "  ${appDocDir.path}/${time}.mp4";

接下来就是完整的给视频添加水印了:

视频添加水印是需要获取视频的分辨率的,使用到的方法是VideoPlayerController。

static Future<String?> _saveNetworkVideoFile(String videoUrl,
      {String? userId, String? nickName}) async {
    var appDocDir = await getTemporaryDirectory();
    String savePath = "${appDocDir.path}/${_getCurrentTime()}.mp4";
    debugPrint("---------resultsavePath=$savePath");
    //下载视频videoUrl是视频路径,savePath是将视频保存到的地址
    await Dio().download(videoUrl, savePath, onReceiveProgress: (count, total) {
      debugPrint("---------${(count / total * 100).toStringAsFixed(0)}%");
    });

    if (userId != null && nickName != null) {
      var videoWidth;
      var videoHeight;
      final File localVideoFile = File(savePath);

      // 初始化VideoPlayerController
      VideoPlayerController _controller =
          VideoPlayerController.file(localVideoFile);

      // 确保视频初始化完成
      await _controller.initialize();
      // 视频加载完元数据后,可以通过value.format.description获取宽高
      if (_controller.value.isInitialized) {
        videoWidth = _controller.value.size.width.toInt();
        videoHeight = _controller.value.size.height.toInt();
        print('Local Video Resolution: $videoWidth x $videoHeight');
      }

      var width = picture.width;
      var height = picture.height;
      var imageWH = videoWidth / width > videoHeight / height
          ? videoHeight / height
          : videoWidth / width;

      var time = _getCurrentTime();
      Uint8List? pngBytes = await _getWatermark(id: userId, name: nickName);
      String _imagePath = appDocDir.path + '/syimage.png'; //水印图片
      File file = File(_imagePath);
      file.writeAsBytes(pngBytes!);

      ///ffmpeg命令生成图片水印
      String command = "-i " +
          savePath +
          " -i " +
          " $_imagePath " +
          "  -filter_complex [1:v]scale=${width * imageWH}:${height * imageWH}[wm];[0:v][wm]overlay=main_w-overlay_w-10:main_h-overlay_h-10 " +
          "  -acodec copy  -q:v 0  -q:a 0 " +
          "  ${appDocDir.path}/${time}.mp4";

      await FFmpegKit.executeAsync(command, (session) async {
        final returnCode = await session.getReturnCode();
        if (ReturnCode.isSuccess(returnCode)) {
          //ffmpeg命令执行成功,保存到本地----自己上网找
        } else {
          //ffmpeg命令执行失败
        }
      });
    } else {
        //没有userId和nickName,直接保存到本地(不添加水印)
    }
  }