尧图精选

C#调用FFmpeg实现录像水印与分辨率控制全攻略

🕒 发布时间:2026/10/1 17:34:13 📁 来源:尧图网络
1. 整体思路C#为什么该用命令行调FFmpeg干上位机的兄弟十有八九迟早会遇到这么一个问题C#写得好好的界面和逻辑突然要加视频录像、要往画面上打水印、还得让用户自己选分辨率。我当时第一反应是找SDK看了几天授权和调用方式心凉了半截用网上那些现成的播放器控件录像又费劲滤镜支持断断续续。后来老老实实把FFmpeg命令行进程接进C#才算把这摊事稳住。今天这篇文章就把当时沉淀下来的东西一次性说清楚——水印怎么打、水印位置怎么算、录音录像时分辨率怎么设以及C#侧拼参数时那些没人提醒你的坑。适合正在做上位机录像、简易录播软件、监控留存工具的工程师也适合打算把FFmpeg命令用起来的新手。1.1 什么场景适合这个方案什么场景不适合我先说清楚边界免得你把方案用错地方。C#通过Process启动ffmpeg.exe本质上是“C#管业务逻辑FFmpeg管音视频处理”这个分工非常清晰。适合的场景有这么几类录像留存、视频加水印、统一输出分辨率、定时录制、按事件触发录制、往流媒体服务器推流。只要你对延迟不是苛刻到几十毫秒级别这个方案都能扛住。不适合的场景也有。比如你想对每一帧做AI推理、实时人脸识别再叠加结果这种逐帧处理走命令行进程就不太合适进程间通信和滤镜回调会把你绕晕。再比如你要做超低延迟的视频通话FFmpeg命令行默认的缓冲策略也不是为这个设计的。判断标准很简单你的核心需求是不是“把一段视频/摄像头流按指定的格式、水印、分辨率处理成文件或流”。如果是命令行方案就是性价比最高的路。为什么不用C#直接调FFmpeg的库比如FFmpeg.AutoGen我试过头文件翻译是一道坎AVFilter的滤镜图结构更是能把人整崩溃一旦内存没管好崩溃日志都看不懂。命令行方案最大的优势是可增量调试先在cmd窗口把命令跑通再交给C#出错范围一下子缩到“参数转义”这一层。对做项目的人来说这比在代码里调滤镜上下文舒服太多。1.2 C#进程封装的最简框架无论后面水印、分辨率怎么折腾进程启动的底子就是这几行。先看代码var psi new ProcessStartInfo { FileName D:\ffmpeg\bin\ffmpeg.exe, UseShellExecute false, CreateNoWindow true, RedirectStandardInput true, RedirectStandardError true, RedirectStandardOutput true }; var proc new Process { StartInfo psi }; proc.ErrorDataReceived (s, e) Console.WriteLine($[ffmpeg] {e.Data}); proc.Start(); proc.BeginErrorReadLine(); proc.BeginOutputReadLine();这里有几个关键点必须解释清楚。UseShellExecute false是为了能重定向标准输入输出同时避免走Shell启动带来的各种环境变量问题。CreateNoWindow true是防止录视频时弹出黑色cmd窗口这个在交付给客户时尤其重要。RedirectStandardError true则是重中之重——FFmpeg的日志默认全走stderr你要是不重定向程序跑挂了都不知道原因。日志读取一定要用BeginErrorReadLine异步方式。我见过不少人直接用ReadToEnd()同步读结果视频分辨率一高、日志输出一多进程卡死。原因很简单stderr缓冲区满了FFmpeg写不进去整个管线堵住。异步回调可以边写边读缓冲区永远不会填满。1.3 宽高比基础4:3、16:9、5:4从哪来分辨率设置绕不开宽高比这个概念。做上位机软件的建立这个表印象非常有用宽高比常见分辨率典型场景4:3640x480、800x600、1024x768、1280x960、1600x1200老式摄像头、VGA采集卡、工业相机16:91280x720、1920x1080、2560x1440、3840x2160现代网络摄像头、屏幕录制、视频会议5:41280x1024老款液晶屏、部分工控一体机、专用显示设备我之所以把5:4单独拿出来说是因为很多上位机软件的主界面就是1280x1024客户要求“录出来的视频和界面一样方方正正”这时候你拿16:9的摄像头硬怼画面要么拉伸要么裁切非常难看。宽高比直接影响录像观感16:9源视频如果输出到4:3容器要么左右加黑边要么上下裁剪4:3源视频输出到16:9容器要么两侧黑边要么画面拉胖。真正专业的做法是保持内容不变形、用黑边补齐目标分辨率这个后面第三章详细讲。还需要留意的是800x480这类车载屏分辨率、576x760这类竖屏广告机分辨率——它们不属于上面任何一个标准比例但统一处理逻辑是一样的先等比缩放再pad补边。2. 水印和水印位置drawtext与overlay的完整玩法水印是这类项目里最常被提的需求。领导说“给录像加上公司名和电话”你以为很简单实际做起来有几个地方特别容易翻车中文乱码、水印位置跑偏、文字被画面吞掉。这一章我把文字水印和图片水印一次讲透。2.1 一行命令打出干净的文字水印先看最基础的文字水印命令假设要给input.mp4加上“测试水印”四个字ffmpeg -i input.mp4 -vf drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text测试水印:fontsize36:fontcolorwhite0.7:x20:y20 output.mp4逐个解释一下参数fontfile指定字体文件路径。Windows下我推荐C\:/Windows/Fonts/msyh.ttc微软雅黑这个字体中英文都清楚做视频水印很稳。注意路径里的冒号必须写成\:因为FFmpeg滤镜里冒号是参数分隔符不转义的话解析会出错。text是水印内容用单引号包起来可以避免空格导致的问题。如果内容里有冒号或逗号还要再转义这个后面专门说。fontsize字号36在1080p下比较明显720p下建议24~32别一个参数走天下。fontcolorwhite0.7是白色加70%不透明度。水印不宜太扎眼半透明效果更专业。这个命令在cmd里能跑通拿到C#里再拼一层字符串引号就行。最开始调试时建议先把fontcolor设成white1.0纯白不透明确认位置字体都没问题再调透明度省得排查时画面里找不到水印。2.2 位置计算main_w和text_w才是关键水印位置是很多人翻车的重灾区。直接写x20:y20确实可以工作但一旦视频分辨率变了水印位置跟着漂。正确的做法是使用FFmpeg滤镜内置变量做相对计算。drawtext滤镜里两个核心变量main_w和main_h代表主视频的宽和高text_w和text_h代表渲染后文字区域的宽和高。注意不是随便写个w就行在drawtext里w等价于text_w但表达不清晰我建议都用全称。九宫格位置的表达式直接抄这个表位置x表达式y表达式左上2020上中(main_w-text_w)/220右上main_w-text_w-2020左中20(main_h-text_h)/2正中(main_w-text_w)/2(main_h-text_h)/2右中main_w-text_w-20(main_h-text_h)/2左下20main_h-text_h-20下中(main_w-text_w)/2main_h-text_h-20右下main_w-text_w-20main_h-text_h-20注意这里main_w-text_w-20里的20是边距也就是水印距离画面边缘20像素。为什么不是main_w-20因为文字的坐标原点在左上角如果x取main_w-20文字的右边缘就会超出画面20像素实际上看到的是水印左移了文字宽度那么远。这个细节我一开始也搞错过白字直接跑出屏幕外了。如果是竖屏视频或者底部区域需要避开操作栏可以用百分比定位。比如把水印放在画面高度90%的位置y就写main_h*0.9-text_h这个写法在处理576x760这类非标竖屏分辨率时特别好用。2.3 进阶图片水印、时间水印、多行多列文字水印之外图片水印也常见比如公司Logo。图片水印用的是overlay滤镜ffmpeg -i input.mp4 -i logo.png -filter_complex overlayxmain_w-overlay_w-20:y20 output.mp4这里要注意变量名变了因为有两路输入overlay滤镜里覆盖层被记为overlay_w和overlay_h代表logo图的宽高。位置计算逻辑跟drawtext完全一样只是文字变量换成图片变量。如果要同时加图片水印和文字水印用filter_complex把两个滤镜串起来ffmpeg -i input.mp4 -i logo.png -filter_complex overlayx20:y20,drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text测试:fontsize28:fontcolorwhite:xmain_w-text_w-20:y20 output.mp4动态时间水印是另一个高频需求尤其做监控录像。显示当前时间可以用drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text%{pts\:localtime\:1693476000}:fontsize28:fontcoloryellow:x20:y20里面的1693476000是一个Unix时间戳基准FFmpeg会根据这个基准换算当前时间。注意三个冒号都要转义成\:。如果只想显示视频播放进度用text%{pts\:hms}会输出00:00:12.345这样的格式。多行多列水印的做法是重复多个drawtext滤镜用逗号分隔。比如右下角放三行字-vf drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text公司名称:xmain_w-text_w-20:ymain_h-text_h-20,drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text客服电话:xmain_w-text_w-20:ymain_h-text_h-65,drawtextfontfileC\:/Windows/Fonts/msyh.ttc:text备案号:xmain_w-text_w-20:ymain_h-text_h-110行距的规律是y每次往上抬45像素左右具体取决于字号。同一字体同一字号下实际行高基本固定用固定间隔不会错。想做水印跑马灯效果把x写成mod(t*80\,main_wtext_w)-text_w水印就会从右往左滚动不过我建议正式项目少用这种花活看久了眼睛累。3. 录音录像分辨率设置从采集到输出全链路分辨率这关难点不在一个-s 1280x720参数而是搞清楚“采集分辨率”、“滤镜处理分辨率”、“输出编码分辨率”这三层分别在哪里设置。3.1 先查摄像头支持的分辨率别盲猜Windows下用DirectShow采集摄像头第一步永远是确认设备名和支持的分辨率。很多人上来就写-s 1920x1080结果摄像头根本不支持FFmpeg要么报错要么自动切到奇怪的分辨率。列出所有采集设备ffmpeg -list_devices true -f dshow -i dummy列出指定设备支持的分辨率和像素格式ffmpeg -f dshow -list_options true -i videoUSB Camera设备名一般不用猜C#端可以通过DirectShow的枚举接口拿到。如果你项目里已经用了摄像头采集组件直接把设备DisplayName拿过来拼命令行就行一个设备开一个ffmpeg进程互不干扰。多个摄像头同时录像就是每个摄像头起一个进程录到不同文件这套架构简单可靠。C#里怎么调用上面两条枚举命令同样是Process启动读stderr输出。FFmpeg这类工具日志全部走stderr读出来之后按行解析设备名一般在“DirectShow video devices”段落下面。3.2 scale加pad宽高比问题一次解决先纠正一个常见误区很多人设置分辨率用的是-s 1280x720这个参数确实是输出分辨率但它不会保留原始宽高比4:3的画面会被直接拉伸到16:9人脸变成猪腰子脸。正确做法是用scale滤镜保留比例再用pad滤镜补黑边。模板命令-vf scale1280:720:force_original_aspect_ratiodecrease,pad1280:720:(ow-iw)/2:(oh-ih)/2:colorblack参数怎么理解scale1280:720:force_original_aspect_ratiodecrease的意思是在1280x720这个目标范围内按原始比例等比缩放取能放下最大画面结果可能是1280x720等比部分匹配也可能是更小。pad的作用是把scale后的画面放到一个精确1280x720的黑色画布上。(ow-iw)/2是水平偏移量ow和oh是scale之后实际画面的宽高iw和ih是输入画面宽高这里就是scale后的结果。用目标宽减去实际宽除以2就把画面推到水平居中。colorblack是黑边颜色也可以写成color0xRRGGBB自定义颜色比如视频会议用colorwhite更清爽。实际操作里我算过一个典型例子源视频是1280x9604:3要统一成1920x108016:9输出。先等比缩放高度到1080时宽度为1080×4/31440也就是scale后是1440x1080再pad到1920x1080水平方向需要补480像素左右各240像素黑边。最终视频没有任何拉伸变形播放出来就是标准的1920x1080内容居中带左右黑边。反过来16:9的源要转成4:3存储比如源是1920x1080目标1280x960等比缩放后是1280x720pad上下各补120像素黑边。逻辑完全一样。如果你完全不想让代码参与这些计算FFmpeg的表达式会自动帮你算好这也是我建议用这个模板而不是自己写C#计算的原因。3.3 录像与推流场景的参数组合采集录像时经常要同时接摄像头和麦克风。命令里两个-i分别指定视频和音频设备ffmpeg -y -f dshow -i videoUSB Camera -f dshow -i audio麦克风 -vf scale1920:1080:force_original_aspect_ratiodecrease,pad1920:1080:(ow-iw)/2:(oh-ih)/2:colorblack -c:v libx264 -preset veryfast -crf 23 -c:a aac -ar 44100 -ac 2 -pix_fmt yuv420p output.mp4这里有个容易踩的坑摄像头采集分辨率如果设得太高而机器性能一般CPU会被编码器拖垮。我的建议是采集分辨率跟着摄像头原生比例走输出分辨率按客户要的统一规格走中间用滤镜扛。-video_size 1280x720可以单独指定dshow采集分辨率但它和后面的滤镜处理是两层东西别混在一起。编码参数里-preset veryfast是为了保实时性-crf 23是质量和体积的平衡点-pix_fmt yuv420p必须带上不然很多播放器打开视频会花屏或者干脆不支持。音频用aac编码采样率44100足够。上面这条命令如果把输出从output.mp4换成-f flv rtmp://...就是推流场景。做低延迟推流时可以在命令前面加-fflags nobuffer -flags low_delay -max_delay 0配合-preset ultrafast降低编码延迟。但要注意这些参数会牺牲一些压缩率和画质录像留存不建议用这套录像追求的是文件质量推流追求的才是低延迟两者目标不同。4. C#侧集成实录转义、日志与进程停止命令在cmd里跑通只是第一步搬到C#里才是真正容易出幺蛾子的地方。这个章节全部是我踩过的坑能帮你少走一个月的弯路。4.1 参数拼接的转义地狱C#拼接FFmpeg参数最痛苦的就是引号和特殊字符。设备名带空格是最常见的情况比如“USB Camera”这个名字。在cmd里这么写没问题ffmpeg -f dshow -i videoUSB Camera但到了C#里这个双引号本身也是字符串的一部分。如果用普通字符串你要写\如果用了verbatim字符串...双引号要打成两个。推荐后者形式最接近原命令不容易错。示例string args $-y -f dshow -i video{deviceName} -video_size 1280x720 ...;滤镜里的特殊字符是第二层地狱。drawtext的text参数里面如果包含冒号:必须转义成\:因为冒号在滤镜参数里是分隔符。包含逗号也要转义成\,。比如水印内容是“时间: 10:30, 测试”在滤镜里要写成text时间\: 10\:30\, 测试这个转义在C#里还要再穿一层衣服。用verbatim字符串的好处是反斜杠原样保留所以你在C#里写的\:到了ffmpeg那边正好是正确的。这个层级要心里有数cmd原命令、C#字符串、滤镜表达式三层各自有一套规则千万别混。还有%符号。如果你用string.Format去格式化命令字符串%Y-%m-%d这样的时间格式会被当成占位符处理。用$插值字符串就安全得多所以我全程都用$...这种写法能少一半转义问题。我的笨办法但非常有效先在cmd窗口里把整条命令调通然后原封不动复制到C#的$...里把里面的逐个改成把设备名、输出路径替换成变量。改完先打一条简单的录像命令验证进程能启动再逐步加滤镜复杂度。4.2 日志读取和优雅停止录像录像进程运行期间实时日志回调是排查问题的最好窗口。前面说过用BeginErrorReadLine异步读proc.ErrorDataReceived (s, e) { if (!string.IsNullOrEmpty(e.Data)) ShowLog(e.Data); }; proc.BeginErrorReadLine();停止录像的正确姿势是往标准输入里写一个qproc.StandardInput.WriteLine(q); if (!proc.WaitForExit(5000)) proc.Kill();q是FFmpeg的原生停止命令它会等当前帧写完、mp4的moov元数据落盘再退出。如果直接Kill()mp4文件大概率打不开因为这个容器格式的文件头在结尾只有正常结束才会写入。真遇到文件损坏也别慌用修复命令抢救ffmpeg -i broken.mp4 -c copy fixed.mp4很多时候能救回来。这里涉及RedirectStandardInput true很多人会漏掉这行然后发现写q没反应。另外进程退出后记得Dispose()避免句柄泄漏。C#里做了一个录像类把启动、停止、日志回调封装好界面层只管调方法这个模式复制到任何项目都适用。4.3 一个可直接抄的录像方法把前面所有知识点揉到一起给一个完整的C#录像核心类。这是我在项目里用过的精简版去掉业务耦合后大概长这样public class FfmpegRecorder : IDisposable { private readonly string _ffmpegPath D:\ffmpeg\bin\ffmpeg.exe; private Process _proc; public void Start(string videoDevice, string audioDevice, int width, int height, string watermark, string outputFile) { string vf BuildVideoFilter(width, height, watermark); string args $-y -f dshow -i video{videoDevice}; if (!string.IsNullOrEmpty(audioDevice)) args $ -f dshow -i audio{audioDevice}; args $ -vf {vf} -c:v libx264 -preset veryfast -crf 23 $-c:a aac -ar 44100 -ac 2 -pix_fmt yuv420p {outputFile}; var psi new ProcessStartInfo(_ffmpegPath, args) { UseShellExecute false, CreateNoWindow true, RedirectStandardInput true, RedirectStandardError true, RedirectStandardOutput true }; _proc new Process { StartInfo psi }; _proc.ErrorDataReceived (s, e) { if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine($[ffmpeg] {e.Data}); }; _proc.Start(); _proc.BeginErrorReadLine(); _proc.BeginOutputReadLine(); } private static string BuildVideoFilter(int width, int height, string watermark) { string vf $scale{width}:{height}:force_original_aspect_ratiodecrease, $pad{width}:{height}:(ow-iw)/2:(oh-ih)/2:colorblack; if (!string.IsNullOrEmpty(watermark)) { string fontFile C\:/Windows/Fonts/msyh.ttc; string safeText watermark.Replace(:, \\:).Replace(,, \\,); vf $,drawtextfontfile{fontFile}:text{safeText}:fontsize32: $fontcolorwhite0.8:x(main_w-text_w)/2:ymain_h-text_h-40; } return vf; } public void Stop() { if (_proc null) return; try { if (!_proc.HasExited) { _proc.StandardInput.WriteLine(q); if (!_proc.WaitForExit(5000)) _proc.Kill(); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } public void Dispose() Stop(); }这里的BuildVideoFilter会自动处理比例保持和水印位置。水印默认放在正中偏下的位置也就是(main_w-text_w)/2和main_h-text_h-40我觉得这个位置不管是监控画面还是操作演示都合适。需要改位置就调这两个表达式的最终坐标把边距40改成你想要的值或者换成左下右下。5. 常见问题与排查速查表这里我把实际项目中高频遇到的问题整理成一个速查表遇到问题直接对着查比自己瞎试快得多。5.1 水印相关故障排查现象原因解决办法水印文字全是方块乱码字体文件不支持中文或没指定fontfile显式指定fontfile为msyh.ttc或simhei.ttf滤镜报错说找不到字体familydrawtext参数里fontfile路径写错检查路径里的冒号是否转义文件是否存在水印内容里的冒号/逗号导致滤镜解析失败特殊字符没转义text内容里的:写成\:,写成\,水印在1920x1080下正常720p下位置不对坐标写死成固定像素了改用main_w、text_w等相对计算表达式启动录像后画面里找不到水印字号太小、颜色跟背景太接近、透明度太低先调成白色1.0、fontsize48确认能看到再调小5.2 分辨率相关故障排查现象原因解决办法摄像头采集报错“Cannot set”指定分辨率设备不支持用-list_options true查出支持列表视频画面拉伸变形直接用-s而不是scalepad改用scale...:force_original_aspect_ratiodecrease,pad...录像文件能录但打不开进程被直接Killmp4的moov没落盘正常用q停止已损坏用ffmpeg -i bad.mp4 -c copy good.mp4修复播放器打开视频花屏或绿屏缺少-pix_fmt yuv420p编码参数里加上-pix_fmt yuv420p指定了宽度但高度不对只写了-video_size没写滤镜让ffmpeg自动选采集和输出两层都要明确别依赖自动匹配5.3 定位问题的手段排查FFmpeg问题第一原则是看原始日志不要看自己程序包装的错误信息。FFmpeg的日志非常诚实基本都会告诉你是哪一步挂了参数值是什么。我定位问题的顺序是在cmd窗口手动执行同样的命令加-loglevel debug看完整日志。如果cmd下正常、C#下不正常对比C#拼出来的完整参数字符串和cmd命令的差异重点检查引号和转义。用tasklist | findstr ffmpeg看有没有残留的ffmpeg进程有的话说明停止逻辑有漏洞进程没被正常退出。我常用的辅助命令也分享一下# 查看drawtext滤镜支持的全部参数 ffmpeg -h filterdrawtext # 查看当前FFmpeg版本和启用的组件 ffmpeg -version # 查看摄像头支持的分辨率列表 ffmpeg -f dshow -list_options true -i video设备名我实际操作中的体会是这类项目最忌讳把命令写散到各个窗体里。我会把分辨率、水印内容、字体路径、编码参数全部做成配置项界面只是改配置底层命令生成函数保持不变。这样客户今天要加一行水印、明天要换分辨率改配置文件就能交付不用动代码。最后再分享一个小技巧所有新参数组合先在cmd窗口用一小段测试视频验证一遍确认输出文件正常、水印位置正确、分辨率符合预期再写进C#代码。这个习惯让我少调试了无数个“看起来一样但就是不对”的诡异问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →