如何用C#结合ffmpeg.exe为视频添加图片叠加层(附代码)
使用C#结合FFmpeg实现视频图片叠加层的完整方案
1. 指定图片叠加层的位置与尺寸
位置控制
FFmpeg的overlay滤镜通过x和y参数定义图片位置:
- 固定像素定位:
overlay=25:25表示距离视频左上角25x25像素 - 相对位置(基于视频分辨率):
- 右对齐:
overlay=W-w:25(W为视频宽度,w为图片宽度) - 底部居中:
overlay=(W-w)/2:H-h(H为视频高度,h为图片高度) - 百分比定位:
overlay=W*0.1:H*0.9(距离左侧10%、底部10%的位置)
- 右对齐:
尺寸控制
需调整图片尺寸时,在图片输入流后添加scale滤镜:
-filter_complex "[1:v]scale=150:100[scaled_img];[0:v][scaled_img]overlay=25:25"
scale=150:100强制设置图片为150x100像素scale=min(W/4,H/4):-1按比例缩小图片至视频宽/高的1/4(-1自动保持宽高比)
2. 确保图片全程可见(适配分辨率/宽高比变化)
要实现全程可见需注意两点:
- 移除时间限制:删除
overlay滤镜中的enable='between(t,0,20)'参数,该参数仅让图片显示0-20秒;全程显示无需添加enable,或设置为enable='gte(t,0)'。 - 使用相对位置/自适应尺寸:
- 基于视频分辨率的相对值定位,避免固定像素在高分辨率视频中偏居一角、低分辨率视频中超出画面
- 用比例缩放图片,确保不会因视频尺寸过小被裁切
示例FFmpeg参数(全程显示+底部右对齐+自适应尺寸):
-i input.mp4 -i overlay.png -filter_complex "[1:v]scale=min(W/5,H/5):-1[img];[0:v][img]overlay=W-w-10:H-h-10" -c:a copy output.mp4
3. C#中执行FFmpeg并捕获错误/状态
核心要点
- 异步读取
StandardOutput和StandardError,避免进程阻塞 - 监听进程事件实时获取执行状态
- 检查退出码(
ExitCode == 0表示执行成功) - 用临时输出文件避免原文件被占用
修正后的C#代码
private async void button6_Click(object sender, EventArgs e) { var testVideoPath = "temp_input.mp4"; var outputVideoPath = "temp_output.mp4"; var overlayImagePath = "temp_overlay.jpg"; // 生成测试视频(实际项目替换为真实视频路径) await RunFfmpegCommand($"-f lavfi -i nullsrc -t 10 -s {pictureBox2.Width}x{pictureBox2.Height} -r 30 -c:v libx264 {testVideoPath}"); render = true; await Task.Run(async () => { for (int i = 0; i < 10; i++) { // 从PictureBox获取最新图片 Bitmap bmp = null; pictureBox1.Invoke((MethodInvoker)(() => { bmp = (Bitmap)pictureBox1.Image.Clone(); })); // 等待图片更新 while (true) { Bitmap currentBmp = null; pictureBox1.Invoke((MethodInvoker)(() => { currentBmp = (Bitmap)pictureBox1.Image.Clone(); })); if (!bmp.Equals(currentBmp)) { bmp.Dispose(); bmp = currentBmp; break; } await Task.Delay(10); } // 保存图片并释放资源 bmp.Save(overlayImagePath); bmp.Dispose(); // 执行叠加命令 var ffmpegArgs = $"-i {testVideoPath} -i {overlayImagePath} -filter_complex \"[1:v]scale=min(W/5,H/5):-1[img];[0:v][img]overlay=W-w-10:H-h-10\" -c:a copy {outputVideoPath}"; var (exitCode, errorOutput) = await RunFfmpegCommand(ffmpegArgs); // 处理执行错误 if (exitCode != 0) { MessageBox.Show($"FFmpeg执行出错:{errorOutput}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); break; } // 更新输入文件为当前输出结果 File.Delete(testVideoPath); File.Move(outputVideoPath, testVideoPath); } }); // 清理临时文件 if (File.Exists(overlayImagePath)) File.Delete(overlayImagePath); if (File.Exists(testVideoPath)) File.Delete(testVideoPath); if (File.Exists(outputVideoPath)) File.Delete(outputVideoPath); } /// <summary> /// 封装FFmpeg命令执行逻辑,返回退出码和错误输出 /// </summary> private async Task<(int ExitCode, string ErrorOutput)> RunFfmpegCommand(string arguments) { var startInfo = new ProcessStartInfo { FileName = "ffmpeg.exe", Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = new Process { StartInfo = startInfo }; var errorOutputBuilder = new StringBuilder(); // 实时捕获错误输出 process.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) { errorOutputBuilder.AppendLine(e.Data); // 可在此处更新UI状态,比如显示处理进度 // Invoke(() => { lblStatus.Text = e.Data; }); } }; process.Start(); process.BeginErrorReadLine(); await process.StandardOutput.ReadToEndAsync(); await process.WaitForExitAsync(); return (process.ExitCode, errorOutputBuilder.ToString()); }
代码说明
RunFfmpegCommand封装了FFmpeg执行逻辑,异步捕获错误输出,避免UI线程阻塞- 使用临时文件循环处理,解决原文件被占用的问题
- 优化图片更新等待逻辑,用
Task.Delay替代Thread.Sleep,避免线程阻塞 - 实时捕获FFmpeg输出,便于调试和错误提示
内容的提问来源于stack exchange,提问作者Frost Dream
相关产品推荐
相关产品推荐

