惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 至尊龙骑

C# 使用FFmpeg 命令录音录像时设置 水印以及水印位置 FFmpeg 命令 水印以及水印位置 FFmpeg 命令录音录像时分辨率 FFmpeg 查看分辨率以及指定分辨率 C# 中监听 IPv6 回环地址----HttpListener C# 中监听 IPv6 回环地址(Loopback Address)----socket和tcp IPv6 地址 后端设置了跨域但是还是提示跨域问题,原因是这里有两个独立的安全策略在起作用:Chrome和Edge浏览器安全策略强制修改方案 开放所有跨域 ----前端和后端 Win11 上遇到的 WinForm 文件拖拽功能失效,但在 Win10 或其他系统上正常的问题 设置iis的后缀名可以下载 linux执行systemctl enable redis.service 报 Failed to execute operation: Bad message linux redis 8.2.1软件开机启动redis.service与etc下的rc.local配置2种方式 Linux redis 8.2.1源码编译 Linux开机启动设置全攻略 Linux 设置nginx 以及java jar自启动 Linux系统简单源码安装NGINX版本1.28.0 DistributedLock 实现.Net分布式锁 Windows 10\11 离线安装.NET Framework 3.5(包括.NET 2.0和3.0) - 至尊龙骑 跨域处理 utools无法搜索快捷方式和部分软件问题
C# 使用FFmpeg 命令 水印以及水印位置 录音录像时设置分辨率-分辨率一般是宽高比是 4:3 和16:9 少数是 5:4
至尊龙骑 · 2026-03-05 · via 博客园 - 至尊龙骑
### C# 使用FFmpeg 命令 水印以及水印位置 录音录像时设置分辨率-分辨率一般是宽高比是 43 和16:9 少数是 54
```
        /// <summary>
        /// 找到所有的采集设备,按照cameraVIDPID找到人脸验证摄像头
        /// </summary>
        /// <param name="cameraVIDPID">摄像头的pid和vid</param>
        /// <param name="filterCategory">设备类型,1 音频设备 2 录像设备</param>
        /// <returns></returns>
        public static string GetDeviceFromPID(string cameraVIDPID, int filterCategory = 2)
        {
            if (string.IsNullOrEmpty(cameraVIDPID))
                return null;
            string monikerString = null;//摄像头设备名称
            FilterInfoCollection videoDevices = null;
            try
            {
                //找到所有的视频采集设备
                switch (filterCategory)
                {
                    case 1://FilterCategory.AudioInputDevice
                        videoDevices = new FilterInfoCollection(FilterCategory.AudioInputDevice);
                        logger.Info($"音频设备:");
                        break;
                    case 2: //FilterCategory.VideoInputDevice
                        videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
                        logger.Info($"录像设备:");
                        break;
                }

                List<DeviceVideoAudio> videoDevicesList = new List<DeviceVideoAudio>();
                for (int i = 0; i < videoDevices.Count; i++)
                {
                    logger.Info($"录像设备/音频设备:Name={videoDevices[i].Name},MonikerString={videoDevices[i].MonikerString}");

                    videoDevicesList.Add(new DeviceVideoAudio { Name = videoDevices[i].Name, MonikerString = videoDevices[i].MonikerString });
                }
                switch (filterCategory)
                {
                    case 1://FilterCategory.AudioInputDevice
                        monikerString = videoDevicesList.Where(a => a.Name.Contains(cameraVIDPID)).ToList().FirstOrDefault(b => b.Name.Contains(cameraVIDPID)).MonikerString.Replace(":", "_");
                        logger.Info($"匹配到 音频设备:Name={cameraVIDPID},MonikerString={monikerString}");
                        break;
                    case 2: //FilterCategory.VideoInputDevice
                        if (cameraVIDPID.ToLower().Contains("pid") || cameraVIDPID.ToLower().Contains("vid"))
                        {
                            monikerString = videoDevicesList.Where(a => a.MonikerString.Contains("pid") || a.MonikerString.Contains("vid")).ToList().FirstOrDefault(b => b.MonikerString.Contains(cameraVIDPID.ToLower())).MonikerString.Replace(":", "_");
                            logger.Info($"匹配 到 录像设备:vid——pid={cameraVIDPID},MonikerString={monikerString}");
                        }
                        else
                        {
                            //videoDevicesList = videoDevicesList.Where(a => a.Name.Contains(cameraVIDPID)).ToList();
                            monikerString = videoDevicesList.FirstOrDefault(b => b.Name.Contains(cameraVIDPID)).MonikerString.Replace(":", "_");
                            logger.Info($"匹配 到 录像设备:Name={cameraVIDPID},MonikerString={monikerString}");

                        }
                        break;
                }

            }
            catch (Exception ex)
            {
                logger.Error($"GetDeviceFromPID获取音视频设备出错,原因:{ex.InnerException}");
                monikerString = null;
            }
            return monikerString;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="voiceText"></param>
        /// <returns></returns>
        public static bool PlaySpeech(string voiceText)
        {
            #region 初始化语音
            if (SpeechTxt == null)
            {
                try
                {
                    SpeechTxt = new SpeechSynthesizer();
                    // SpeechTxt.SelectVoice("Microsoft Lili");//设置播音员(中文)
                    // SpeechTxt.SelectVoice("Microsoft Anna"); //英文
                    // SpeechTxt.Volume = 100; //音量
                    var cultureInfo = System.Windows.Forms.InputLanguage.CurrentInputLanguage.Culture ?? Thread.CurrentThread.CurrentCulture;
                    VoiceInfo voiceInfo = SpeechTxt.GetInstalledVoices(cultureInfo)?.FirstOrDefault()?.VoiceInfo;
                    SpeechTxt.SelectVoice(voiceInfo.Name ?? "Microsoft Lili");
                }
                catch (Exception ex)
                {
                    logger.Error($"初始化语音出错,原因:{ex.InnerException}");
                    SpeechTxt = null;
                }
            }
            #endregion

            bool result = false;

            if (string.IsNullOrWhiteSpace(voiceText))
            {
                return result;
            }

            try
            {
                //SpeechTxt.Speak(voiceText);
                SpeechTxt.SpeakAsync(voiceText);
                result = true;
            }
            catch (Exception ex)
            {
                logger.Error($"PlaySpeech,失败,原因:{ex.Message}");
                result = false;
            }
            return result;
        }

        public static bool KillProcess(string prcessName, string command = "taskkill /IM ffmpeg.exe /F")
        {
            bool result = false;
            System.Diagnostics.Process p = null;
            try
            {
                if (!string.IsNullOrWhiteSpace(prcessName))
                {
                    System.Diagnostics.Process[] processList = System.Diagnostics.Process.GetProcesses();
                    foreach (System.Diagnostics.Process process in processList)
                    {

                        if (process.ProcessName.ToLower() == prcessName.ToLower())
                        {
                            process.Kill(); //结束进程                   
                        }
                    }
                    result = true;
                }
                else
                {
                    ////ProcessRecord.StandardInput.WriteLine("taskkill /IM ffmpeg.exe /F");
                    p = new System.Diagnostics.Process();
                    p.StartInfo.FileName = "cmd.exe"; //设定程序名
                    p.StartInfo.Arguments = "/c " + command; //设定程式执行参数
                    p.StartInfo.UseShellExecute = false; //关闭Shell的使用
                    p.StartInfo.RedirectStandardInput = true; //重定向标准输入
                    p.StartInfo.RedirectStandardOutput = true; //重定向标准输出
                    p.StartInfo.RedirectStandardError = true; //重定向错误输出
                    p.StartInfo.CreateNoWindow = true; //设置不显示窗口
                    p.Start(); //启动
                    result = true;
                }
            }
            catch (Exception ex)
            {
                logger.Error($"KillProcess,失败,原因:{ex.Message}");
                //ProcessRecord.StandardInput.WriteLine("taskkill /IM ffmpeg.exe /F");
                result = false;
            }
            finally
            {
                p.WaitForExit();
                p.Close();
                p.Dispose();
            }
            return result;
        }

        /// <summary>
        /// 停止录像
        /// </summary>
        /// <returns></returns>
        public static bool StopRecord()
        {
            bool result = false;
            try
            {
                //string a = "q";

                ////ProcessRecord.StartInfo.RedirectStandardInput = true; // 重定向标准输入
                //// 写入数据到标准输入
                //using (StreamWriter sw = ProcessRecord.StandardInput)
                //{
                //    if (sw.BaseStream.CanWrite)
                //    {
                //        sw.WriteLine(a);
                //        sw.WriteLine("exit");
                //    }
                //}
                if (ProcessRecord == null)
                {
                    PlaySpeech("录音录像未开启,请先开启录音录像设备!");
                    return false;
                }
                ProcessRecord.StandardInput.WriteLine("q");
                ProcessRecord.StandardInput.WriteLine("exit");
                ProcessRecord.WaitForExit();
                ProcessRecord.Close();
                ProcessRecord.Dispose();
                Untils.PlaySpeech("录音录像已关闭");
                result = true;

            }
            catch (Exception ex)
            {
                logger.Error($"StopRecord,失败,原因:{ex.Message}");
                ProcessRecord.WaitForExit();
                ProcessRecord.Close();
                ProcessRecord.Dispose();
                //Untils.PlaySpeech("录音录像已关闭");
                //ProcessRecord.StandardInput.WriteLine("taskkill /IM ffmpeg.exe /F");
                result = false;
            }
            return result;
        }

        /// <summary>
        /// 开始录像
        /// </summary>
        /// <returns></returns>
        public static bool StartRecord()
        {
            bool result = false;
            try
            {
                //StopRecord();
                KillProcess(null);

                //string videoInput = GetSetting("RecordVideo");
                //string audioInput = GetSetting("RecordAudio");
                //logger.Info($"StartRecord,录像:{videoInput}");
                //logger.Info($"StartRecord,录音:{audioInput}");

                string videoInput = GetSetting("RecordVideoPID");
                string audioInput = GetSetting("RecordAudioPID");
                //string videoInput = GetSetting("FisheyeRecordVideoCameraPID");
                videoInput = GetDeviceFromPID(videoInput, 2);
                //logger.Info($"StartRecord,录音录像-录像设备:{videoInput}");

                //string audioInput = GetSetting("FisheyeRecordAudioPID");
                audioInput = GetDeviceFromPID(audioInput, 1);
                ////audioInput = GetSetting("RecordAudio");
                //logger.Info($"StartRecord,录音录像-录音设备:{audioInput}");

                string videoSaveFormat = GetSetting("RecordAudio");
                if (string.IsNullOrWhiteSpace(videoInput))
                {
                    //MessageBox.Show("未选择录像设备", "提示:");

                    return result;
                }

                if (string.IsNullOrWhiteSpace(audioInput))
                {
                    //MessageBox.Show("未选择录音设备", "提示:");

                    return result;
                }

                if (string.IsNullOrWhiteSpace(videoSaveFormat))
                {
                    //MessageBox.Show("未选择录音录像设备的视频录制格式", "提示:");

                    return result;
                }


                #region C# 代码用于通过 Process 调用 FFmpeg 进行音视频采集(使用 dshow 设备)并叠加时间水印,
                //整体结构基本正确,但存在几个关键问题和优化建议,可能导致:
                //FFmpeg 启动失败
                //音视频设备无法识别
                //中文路径 / 特殊字符问题
                //进程无法正常结束或卡死
                //string ffmpegPath = "ffmpeg.exe";//ffmpeg路径
                ////string videoFilePath = $"{DateTime.Now.ToString("yyyyMMddHHmmss")}.mp4";//视频地址
                //string videoFilePath = "123.mp4";//视频地址
                //ProcessRecord = new Process();
                //ProcessStartInfo startInfo = new ProcessStartInfo();
                //startInfo.FileName = ffmpegPath;
                //string drawStr = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
                //startInfo.Arguments = $@"-rtbufsize 200M -f dshow -i video=""{videoInput}"" -f dshow -i audio=""{audioInput}"" -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 -vf  drawtext=fontsize=56:x=100:y=100:fontcolor=red:text='{drawStr}' -y {videoFilePath}";
                //startInfo.UseShellExecute = false;
                //startInfo.CreateNoWindow = true;
                //startInfo.RedirectStandardInput = true;
                //startInfo.RedirectStandardOutput = true;
                //startInfo.RedirectStandardError = true;
                //startInfo.RedirectStandardInput = true;
                //startInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory;
                //ProcessRecord.StartInfo = startInfo;
                //ProcessRecord.OutputDataReceived += Process_OutputDataReceived;
                //ProcessRecord.ErrorDataReceived += Process_ErrorDataReceived;
                ////process.StartInfo.Arguments = a;
                //ProcessRecord.Start();
                //ProcessRecord.BeginErrorReadLine();
                //ProcessRecord.BeginOutputReadLine(); 

                //lblffmepg.ForeColor = Color.Green;
                //lblffmepg.Text = "开始录音录像";
                #endregion

                //ffmpeg 指令执行格式:ffmpeg [输入选项] [滤镜选项] 输出文件路径
                string ffmpegPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg.exe");
                if (!File.Exists(ffmpegPath))
                    logger.Error($"未找到 ffmpeg.exe--{ffmpegPath}");

                //string drawStr = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
                //// 转义文本中的单引号(FFmpeg drawtext 要求)--- 将 text 中的 ' 替换为 '\''(FFmpeg 要求)
                //string safeText = drawStr.Replace("'", "'\\''");
                //string videoFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{DateTime.Now.ToString("yyyyMMddHHmmss")}.mp4");//视频地址
                //// 输出路径加引号防空格 输出文件名(带引号防止空格或特殊字符问题) 更推荐:对整个文本值 值使用双引号 + 转义(FFmpeg 支持)
                //string safeOutput = $"\"{videoFilePath ?? "123.mp4"}\"";

                //var startInfo = new ProcessStartInfo
                //{
                //    FileName = ffmpegPath,
                //    // 将 text 中的 ' 替换为 '\''(FFmpeg 要求)
                //    //Arguments = $@"-rtbufsize 200M -f dshow -i video=""{videoInput}"" -f dshow -i audio=""{audioInput}"" -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 -vf drawtext=fontsize=56:x=100:y=100:fontcolor=red:text='{safeText}' -y {safeOutput}",
                //    //更推荐:对整个 text 值使用双引号 + 转义(FFmpeg 支持):string safeText = drawStr.Replace("\"", "\\\"");    ... text =\"{safeText}\" ...
                //    Arguments = $@"-rtbufsize 200M -f dshow -i video=""{videoInput}"" -f dshow -i audio=""{audioInput}"" -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 -vf drawtext=fontsize=56:x=100:y=100:fontcolor=red:text=\""{safeText}\"" -y {safeOutput}",

                //    UseShellExecute = false,
                //    CreateNoWindow = true,
                //    RedirectStandardInput = true,
                //    RedirectStandardOutput = true,
                //    RedirectStandardError = true,
                //    WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
                //};

                //ProcessRecord = new Process { StartInfo = startInfo };
                //ProcessRecord.OutputDataReceived += Process_OutputDataReceived;
                //ProcessRecord.ErrorDataReceived += Process_ErrorDataReceived;
                ////process.StartInfo.Arguments = a;
                //ProcessRecord.Start();
                //ProcessRecord.BeginErrorReadLine();
                //ProcessRecord.BeginOutputReadLine();

                //启动后不要立即 dispose!等 Exited 事件触发
                //ProcessRecord.EnableRaisingEvents = true;
                //ProcessRecord.Exited += (s, e) =>
                //{
                //    Console.WriteLine($"FFmpeg 退出,退出码: {ProcessRecord.ExitCode}");
                //    ProcessRecord.Dispose();
                //};

                #region 使用 ProcessStartInfo.ArgumentList(.NET Core 2.1+ / .NET 5+ 推荐)C# 中安全传递带引号的路径
                //var startInfo = new ProcessStartInfo
                //{
                //    FileName = "ffmpeg.exe",
                //    UseShellExecute = false,
                //    CreateNoWindow = true
                //};

                //// 直接添加参数,无需手动加引号!
                //startInfo.ArgumentList.Add("-rtbufsize");
                //startInfo.ArgumentList.Add("200M");

                //startInfo.ArgumentList.Add("-f");
                //startInfo.ArgumentList.Add("dshow");
                //startInfo.ArgumentList.Add("-i");
                //startInfo.ArgumentList.Add($"video={videoInput}"); // videoInput 已包含完整设备字符串

                //startInfo.ArgumentList.Add("-f");
                //startInfo.ArgumentList.Add("dshow");
                //startInfo.ArgumentList.Add("-i");
                //startInfo.ArgumentList.Add($"audio={audioInput}");

                //startInfo.ArgumentList.Add("-pix_fmt");
                //startInfo.ArgumentList.Add("yuv420p");
                //startInfo.ArgumentList.Add("-tune");
                //startInfo.ArgumentList.Add("zerolatency");
                //startInfo.ArgumentList.Add("-ac");
                //startInfo.ArgumentList.Add("1");
                //startInfo.ArgumentList.Add("-ar");
                //startInfo.ArgumentList.Add("8000");
                //startInfo.ArgumentList.Add("-ab");
                //startInfo.ArgumentList.Add("44100");

                //startInfo.ArgumentList.Add("-vf");
                //startInfo.ArgumentList.Add($"drawtext=fontsize=56:x=100:y=100:fontcolor=red:text='{DateTime.Now:yyyy-MM-dd-HH-mm-ss}'");

                //startInfo.ArgumentList.Add("-y");
                //startInfo.ArgumentList.Add(videoFilePath); // ← 直接传路径,自动处理引号!

                //Process.Start(startInfo); 
                #endregion

                #region 如果必须用 .Arguments(.NET Framework 或旧版)

                var startInfo = new ProcessStartInfo
                {
                    FileName = ffmpegPath,
                    // 将 text 中的 ' 替换为 '\''(FFmpeg 要求)
                    //Arguments = $@"-rtbufsize 200M -f dshow -i video=""{videoInput}"" -f dshow -i audio=""{audioInput}"" -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 -vf drawtext=fontsize=56:x=100:y=100:fontcolor=red:text='{safeText}' -y {safeOutput}",
                    //更推荐:对整个 text 值使用双引号 + 转义(FFmpeg 支持):string safeText = drawStr.Replace("\"", "\\\"");    ... text =\"{safeText}\" ...
                    //Arguments = $@"-rtbufsize 200M -f dshow -i video=""{videoInput}"" -f dshow -i audio=""{audioInput}"" -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 -vf drawtext=fontsize=56:x=100:y=100:fontcolor=red:text=\""{safeText}\"" -y {safeOutput}",

                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
                };
                string outputFileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{DateTime.Now:yyyyMMddHHmmss}.mp4");//视频地址 DateTime.Now.ToString("yyyyMMddHHmmss") + ".mp4";
                string safeOutputPath = $"\"{outputFileName}\""; // 手动加双引号,输出路径用 \"...\" 包裹

                // 构建完整参数字符串(注意 drawtext 的 text 也要处理)text 中的 ' 要转义为 \'(FFmpeg 要求)
                string text = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss").Replace("'", "\\'");
                //水印时间---没有双引号 //整个 -vf 参数值用 \"...\" 包裹(因为含 : 和 =)
                //string vf = $"drawtext=fontsize=56:x=100:y=100:fontcolor=red:text='{text}'";
                //设置时间水印居中
                string vf = $"drawtext=fontsize=56:fontcolor=red:text='{text}':x=(w-text_w)/2:y=(h-text_h)/2";
                //w = 实际视频宽度(如 1280),h = 实际视频高度(如 720),text_w, text_h = 文字渲染后的像素尺寸, 无需知道具体分辨率,表达式会自动适配!

                #region MyRegion
                //FFmpeg 命令 水印 以及水印位置
                //水印时间---有双引号
                //string vf = $"drawtext=fontsize=56:x=100:y=100:fontcolor=red:text=\"{text}\"";//整个 -vf 参数值用 \"...\" 包裹(因为含 : 和 =)
                //要在 FFmpeg 的 drawtext 滤镜中将文字居中显示在视频正中心,不能使用固定的 x = 100:y = 100,而应使用 动态表达式,基于视频的宽度(w)和高度(h)以及文本自身的尺寸(text_w, text_h)来计算位置。
                //居中文本(水平 + 垂直居中)drawtext=fontsize=56:fontcolor=red:text='你的文字':x=(w-text_w)/2:y=(h-text_h)/2
                //参数说明:
                //| 表达式 | 含义 |
                //| --------| ------|
                //| `w` | 视频宽度(pixels) |
                //| `h` | 视频高度(pixels) |
                //| `text_w` | 当前文本渲染后的宽度 |
                //| `text_h` | 当前文本渲染后的高度 |
                //| `(w - text_w) / 2` | 水平居中:左边距 = (总宽 - 文字宽) ÷ 2 |
                //| `(h - text_h) / 2` | 垂直居中:上边距 = (总高 - 文字高) ÷ 2 |

                //其他常见居中需求
                //| 需求 | 表达式 |
                //| ------| --------|
                //| 水平居中,顶部对齐 | `x = (w - text_w) / 2:y = 0` |
                //| 水平居中,底部对齐 | `x = (w - text_w) / 2:y = h - text_h` |
                //| 垂直居中,左对齐 | `x = 0:y = (h - text_h) / 2` |
                //| 垂直居中,右对齐 | `x = w - text_w:y = (h - text_h) / 2` |
                //| 带边距的居中(如上下留 50px) | `x = (w - text_w) / 2:y = (h - text_h) / 2 - 50` |

                //⚠️注意事项
                //必须指定字体(可选但推荐)
                //默认字体可能不支持中文或样式不一致,建议指定:
                //bash
                //:fontfile =/ path / to / arial.ttf
                //Windows 示例:
                //bash
                //:fontfile = C\\:/ Windows / Fonts / arial.ttf
                //中文显示为方框?
                //确保字体支持中文(如 msyh.ttc 微软雅黑)
                //路径中的 \ 要转义为 \\ 或 /
                //bash
                //: fontfile = C\\:/ Windows / Fonts / msyh.ttc
                //实时流(dshow)中 text_w/ text_h 可能延迟生效
                //在录制开始的前几帧,文本尺寸可能未计算完成,导致短暂偏移(通常 1~2 帧后正常)。
                //✅ 最终命令示例(完整)
                //bash
                //ffmpeg -rtbufsize 200M -f dshow -i video="..." -f dshow -i audio="..." \
                //  -pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 \
                //  -vf "drawtext=fontsize=56:fontcolor=red:text='2026-03-04 14:30:00':x=(w-text_w)/2:y=(h-text_h)/2" \
                //  -y output.mp4
                //这样文字就会完美居中在视频画面中央!

                //FFmpeg 命令录音录像时分辨率
                //FFmpeg 命令录音录像时候是没有固定“默认”分辨率!它完全取决于你的 DirectShow 视频采集设备(摄像头)的当前输出格式。
                //🔍 详细解释
                //- f dshow 表示使用 Windows 的 DirectShow 接口采集音视频。
                //FFmpeg 不会自动设置分辨率,而是直接使用设备当前激活的媒体类型(media type)中的分辨率。
                //常见情况:
                //表格
                //设备类型    可能的默认分辨率
                //笔记本内置摄像头    640x480(VGA)、1280x720(HD)
                //USB 摄像头 640x480、1280x720、1920x1080(取决于驱动和设备能力)
                //虚拟摄像头(OBS - VirtualCam 等)    用户自定义(如 1920x1080)
                //⚠️ 同一个摄像头,在不同软件中可能输出不同分辨率(因为每个软件会请求不同的格式)。

                #endregion
                //startInfo.Arguments = "-rtbufsize 200M " + $"-f dshow -i \"video={videoInput}\" " + $"-f dshow -i \"audio={audioInput}\" " + "-pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 " + $"-vf \"{vf}\" " + "-y " + safeOutputPath;
                //强制指定分辨率,设置时间水印居中 ,在 -i 参数前使用 -video_size强制指定分辨率
                startInfo.Arguments = "-rtbufsize 200M " + $"-f dshow -video_size 1280x720 -framerate 30 -i \"video={videoInput}\" " + $"-f dshow -i \"audio={audioInput}\" " + "-pix_fmt yuv420p -tune zerolatency -ac 1 -ar 8000 -ab 44100 " + $"-vf \"{vf}\" " + "-y " + safeOutputPath;
  //              var sss =
  //                  ffmpeg - rtbufsize 200M \
  //-f dshow - video_size 1280x720 - framerate 30 - i video = "..." \
  //-f dshow - i audio = "..." \
  //-pix_fmt yuv420p - tune zerolatency \
  //-ac 1 - ar 8000 - ab 44100 \
  //-vf "drawtext=fontsize=56:fontcolor=red:text='2026-03-04 14:30:00':x=(w-text_w)/2:y=(h-text_h)/2" \
  //-y output.mp4

                //FFmepeg使用注意
                //1、text 中的 ' 要转义为 \'(FFmpeg 要求)
                //2、输出路径用 \"...\" 包裹
                //整个 -vf 参数值用 \"...\" 包裹(因为含 : 和 =)
                ProcessRecord = new Process { StartInfo = startInfo };
                ProcessRecord.OutputDataReceived += Process_OutputDataReceived;
                ProcessRecord.ErrorDataReceived += Process_ErrorDataReceived;
                //process.StartInfo.Arguments = a;
                ProcessRecord.Start();
                ProcessRecord.BeginErrorReadLine();
                ProcessRecord.BeginOutputReadLine();
                #endregion

                Untils.PlaySpeech("录音录像已开启");
                result = true;
            }
            catch (Exception ex)
            {
                logger.Error($"StartRecord,失败,原因:{ex.Message}");
                result = false;
            }
            return result;
        }

        public static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            logger.Info($"StartRecord,录音录像 OutputDataReceived:{e.Data}");
            //Log(e.Data, MessageType.info);
        }

        public static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            logger.Error($"StartRecord,录音录像 ErrorDataReceived,原因:{e.Data}");
        }
```